feat(selector): add prev/getPrev

This commit is contained in:
lisonge 2024-07-11 21:35:48 +08:00
parent bbd52a2f35
commit c3d7969ac9
27 changed files with 497 additions and 340 deletions

View File

@ -7,6 +7,7 @@ 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.Context
import li.songe.selector.MismatchExpressionTypeException
import li.songe.selector.MismatchOperatorTypeException
import li.songe.selector.MismatchParamTypeException
@ -95,9 +96,8 @@ fun AccessibilityNodeInfo.querySelector(
emptyList()
})
if (nodes.isNotEmpty()) {
val trackNodes = ArrayList<AccessibilityNodeInfo>(selector.tracks.size)
nodes.forEach { childNode ->
val targetNode = selector.match(childNode, transform, trackNodes)
val targetNode = selector.match(childNode, transform)
if (targetNode != null) return targetNode
}
}
@ -232,6 +232,14 @@ class NodeCache {
indexMap.evictAll()
}
fun getRoot(node: AccessibilityNodeInfo): AccessibilityNodeInfo? {
if (rootNode == null) {
rootNode = GkdAbService.service?.safeActiveWindow
}
if (node == rootNode) return null
return rootNode
}
val sizeList: List<Int>
get() = listOf(childMap.size(), parentMap.size(), indexMap.size())
@ -310,13 +318,16 @@ fun createCacheTransform(): CacheTransform {
}
val getNodeAttr = createGetNodeAttr(cache)
val transform = Transform(
getAttr = { node, name ->
when (node) {
is AccessibilityNodeInfo -> getNodeAttr(node, name)
is CharSequence -> getCharSequenceAttr(node, name)
else -> {
null
getAttr = { target, name ->
when (target) {
is Context<*> -> when (name) {
"prev" -> target.prev
else -> getNodeAttr(target.current as AccessibilityNodeInfo, name)
}
is AccessibilityNodeInfo -> getNodeAttr(target, name)
is CharSequence -> getCharSequenceAttr(target, name)
else -> null
}
},
getInvoke = { target, name, args ->
@ -331,6 +342,20 @@ fun createCacheTransform(): CacheTransform {
else -> null
}
is Context<*> -> when (name) {
"getPrev" -> {
args.getIntOrNull()?.let { target.getPrev(it) }
}
"getChild" -> {
args.getIntOrNull()?.let { index ->
cache.getChild(target.current as AccessibilityNodeInfo, index)
}
}
else -> null
}
is CharSequence -> getCharSequenceInvoke(target, name, args)
is Int -> getIntInvoke(target, name, args)
is Boolean -> getBooleanInvoke(target, name, args)
@ -342,6 +367,7 @@ fun createCacheTransform(): CacheTransform {
getName = { node -> node.className },
getChildren = getChildrenCache,
getParent = { cache.getParent(it) },
getRoot = { cache.getRoot(it) },
getDescendants = { node ->
sequence {
val stack = getChildrenCache(node).toMutableList()
@ -349,7 +375,7 @@ fun createCacheTransform(): CacheTransform {
stack.reverse()
val tempNodes = mutableListOf<AccessibilityNodeInfo>()
do {
val top = stack.removeLast()
val top = stack.removeAt(stack.lastIndex)
yield(top)
for (childNode in getChildrenCache(top)) {
tempNodes.add(childNode)
@ -363,7 +389,7 @@ fun createCacheTransform(): CacheTransform {
} while (stack.isNotEmpty())
}.take(MAX_DESCENDANTS_SIZE)
},
getChildrenX = { node, connectExpression ->
traverseChildren = { node, connectExpression ->
sequence {
repeat(node.childCount.coerceAtMost(MAX_CHILD_SIZE)) { offset ->
connectExpression.maxOffset?.let { maxOffset ->
@ -376,7 +402,7 @@ fun createCacheTransform(): CacheTransform {
}
}
},
getBeforeBrothers = { node, connectExpression ->
traverseBeforeBrothers = { node, connectExpression ->
sequence {
val parentVal = cache.getParent(node) ?: return@sequence
// 如果 node 由 quickFind 得到, 则第一次调用此方法可能得到 cache.index 是空
@ -407,7 +433,7 @@ fun createCacheTransform(): CacheTransform {
}
}
},
getAfterBrothers = { node, connectExpression ->
traverseAfterBrothers = { node, connectExpression ->
val parentVal = cache.getParent(node)
if (parentVal != null) {
val index = cache.indexMap[node]
@ -447,7 +473,7 @@ fun createCacheTransform(): CacheTransform {
emptySequence()
}
},
getDescendantsX = { node, connectExpression ->
traverseDescendants = { node, connectExpression ->
sequence {
val stack = getChildrenCache(node).toMutableList()
if (stack.isEmpty()) return@sequence
@ -455,7 +481,7 @@ fun createCacheTransform(): CacheTransform {
val tempNodes = mutableListOf<AccessibilityNodeInfo>()
var offset = 0
do {
val top = stack.removeLast()
val top = stack.removeAt(stack.lastIndex)
if (connectExpression.checkOffset(offset)) {
yield(top)
}
@ -493,11 +519,14 @@ fun createNoCacheTransform(): CacheTransform {
val transform = Transform(
getAttr = { target, name ->
when (target) {
is Context<*> -> when (name) {
"prev" -> target.prev
else -> getNodeAttr(target.current as AccessibilityNodeInfo, name)
}
is AccessibilityNodeInfo -> getNodeAttr(target, name)
is CharSequence -> getCharSequenceAttr(target, name)
else -> {
null
}
else -> null
}
},
getInvoke = { target, name, args ->
@ -512,6 +541,20 @@ fun createNoCacheTransform(): CacheTransform {
else -> null
}
is Context<*> -> when (name) {
"getPrev" -> {
args.getIntOrNull()?.let { target.getPrev(it) }
}
"getChild" -> {
args.getIntOrNull()?.let { index ->
cache.getChild(target.current as AccessibilityNodeInfo, index)
}
}
else -> null
}
is CharSequence -> getCharSequenceInvoke(target, name, args)
is Int -> getIntInvoke(target, name, args)
is Boolean -> getBooleanInvoke(target, name, args)
@ -529,7 +572,7 @@ fun createNoCacheTransform(): CacheTransform {
val tempNodes = mutableListOf<AccessibilityNodeInfo>()
var offset = 0
do {
val top = stack.removeLast()
val top = stack.removeAt(stack.lastIndex)
yield(top)
offset++
if (offset > MAX_DESCENDANTS_SIZE) {
@ -547,7 +590,7 @@ fun createNoCacheTransform(): CacheTransform {
} while (stack.isNotEmpty())
}
},
getChildrenX = { node, connectExpression ->
traverseChildren = { node, connectExpression ->
sequence {
repeat(node.childCount.coerceAtMost(MAX_CHILD_SIZE)) { offset ->
connectExpression.maxOffset?.let { maxOffset ->

View File

@ -131,10 +131,10 @@ class AppConfigVm @Inject constructor(stateHandle: SavedStateHandle) : ViewModel
private val appConfigsFlow = subsFlow.map { subs ->
DbSet.subsConfigDao.queryAppConfig(subs.map { it.first.id }, args.appId)
}.flatMapLatest { it }
}.flatMapLatest { it }.stateIn(viewModelScope, SharingStarted.Eagerly, emptyList())
private val categoryConfigsFlow = subsFlow.map { subs ->
DbSet.categoryConfigDao.queryBySubsIds(subs.map { it.first.id })
}.flatMapLatest { it }
}.flatMapLatest { it }.stateIn(viewModelScope, SharingStarted.Eagerly, emptyList())
val appGroupsFlow = combine(
sortedAppGroupsFlow,
appConfigsFlow,

View File

@ -1,3 +1,4 @@
import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
plugins {
@ -7,6 +8,7 @@ plugins {
kotlin {
jvm {
@OptIn(ExperimentalKotlinGradlePluginApi::class)
compilerOptions {
jvmTarget.set(JvmTarget.JVM_17)
}

View File

@ -10,8 +10,11 @@ data class BinaryExpression(
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 fun <T> match(
context: Context<T>,
transform: Transform<T>,
): Boolean {
return operator.value.compare(context, transform, left, right)
}
override val binaryExpressions

View File

@ -7,10 +7,10 @@ sealed class CompareOperator(val key: String) : Stringify {
override fun stringify() = key
internal abstract fun <T> compare(
node: T,
context: Context<T>,
transform: Transform<T>,
leftExp: ValueExpression,
rightExp: ValueExpression
rightExp: ValueExpression,
): Boolean
internal abstract fun allowType(left: ValueExpression, right: ValueExpression): Boolean
@ -51,13 +51,13 @@ sealed class CompareOperator(val key: String) : Stringify {
data object Equal : CompareOperator("=") {
override fun <T> compare(
node: T,
context: Context<T>,
transform: Transform<T>,
leftExp: ValueExpression,
rightExp: ValueExpression
rightExp: ValueExpression,
): Boolean {
val left = leftExp.getAttr(node, transform)
val right = rightExp.getAttr(node, transform)
val left = leftExp.getAttr(context, transform)
val right = rightExp.getAttr(context, transform)
return if (left is CharSequence && right is CharSequence) {
left.contentReversedEquals(right)
} else {
@ -70,12 +70,12 @@ sealed class CompareOperator(val key: String) : Stringify {
data object NotEqual : CompareOperator("!=") {
override fun <T> compare(
node: T,
context: Context<T>,
transform: Transform<T>,
leftExp: ValueExpression,
rightExp: ValueExpression
rightExp: ValueExpression,
): Boolean {
return !Equal.compare(node, transform, leftExp, rightExp)
return !Equal.compare(context, transform, leftExp, rightExp)
}
override fun allowType(left: ValueExpression, right: ValueExpression) = true
@ -83,13 +83,14 @@ sealed class CompareOperator(val key: String) : Stringify {
data object Start : CompareOperator("^=") {
override fun <T> compare(
node: T,
context: Context<T>,
transform: Transform<T>,
leftExp: ValueExpression,
rightExp: ValueExpression
): Boolean {
val left = leftExp.getAttr(node, transform)
val right = rightExp.getAttr(node, transform)
rightExp: ValueExpression,
): Boolean {
val left = leftExp.getAttr(context, transform)
val right = rightExp.getAttr(context, transform)
return if (left is CharSequence && right is CharSequence) {
left.startsWith(right)
} else {
@ -104,13 +105,14 @@ sealed class CompareOperator(val key: String) : Stringify {
data object NotStart : CompareOperator("!^=") {
override fun <T> compare(
node: T,
context: Context<T>,
transform: Transform<T>,
leftExp: ValueExpression,
rightExp: ValueExpression
): Boolean {
val left = leftExp.getAttr(node, transform)
val right = rightExp.getAttr(node, transform)
rightExp: ValueExpression,
): Boolean {
val left = leftExp.getAttr(context, transform)
val right = rightExp.getAttr(context, transform)
return if (left is CharSequence && right is CharSequence) {
!left.startsWith(right)
} else {
@ -124,13 +126,14 @@ sealed class CompareOperator(val key: String) : Stringify {
data object Include : CompareOperator("*=") {
override fun <T> compare(
node: T,
context: Context<T>,
transform: Transform<T>,
leftExp: ValueExpression,
rightExp: ValueExpression
): Boolean {
val left = leftExp.getAttr(node, transform)
val right = rightExp.getAttr(node, transform)
rightExp: ValueExpression,
): Boolean {
val left = leftExp.getAttr(context, transform)
val right = rightExp.getAttr(context, transform)
return if (left is CharSequence && right is CharSequence) {
left.contains(right)
} else {
@ -144,13 +147,14 @@ sealed class CompareOperator(val key: String) : Stringify {
data object NotInclude : CompareOperator("!*=") {
override fun <T> compare(
node: T,
context: Context<T>,
transform: Transform<T>,
leftExp: ValueExpression,
rightExp: ValueExpression
): Boolean {
val left = leftExp.getAttr(node, transform)
val right = rightExp.getAttr(node, transform)
rightExp: ValueExpression,
): Boolean {
val left = leftExp.getAttr(context, transform)
val right = rightExp.getAttr(context, transform)
return if (left is CharSequence && right is CharSequence) {
!left.contains(right)
} else {
@ -164,13 +168,14 @@ sealed class CompareOperator(val key: String) : Stringify {
data object End : CompareOperator("$=") {
override fun <T> compare(
node: T,
context: Context<T>,
transform: Transform<T>,
leftExp: ValueExpression,
rightExp: ValueExpression
): Boolean {
val left = leftExp.getAttr(node, transform)
val right = rightExp.getAttr(node, transform)
rightExp: ValueExpression,
): Boolean {
val left = leftExp.getAttr(context, transform)
val right = rightExp.getAttr(context, transform)
return if (left is CharSequence && right is CharSequence) {
left.endsWith(right)
} else {
@ -184,13 +189,14 @@ sealed class CompareOperator(val key: String) : Stringify {
data object NotEnd : CompareOperator("!$=") {
override fun <T> compare(
node: T,
context: Context<T>,
transform: Transform<T>,
leftExp: ValueExpression,
rightExp: ValueExpression
): Boolean {
val left = leftExp.getAttr(node, transform)
val right = rightExp.getAttr(node, transform)
rightExp: ValueExpression,
): Boolean {
val left = leftExp.getAttr(context, transform)
val right = rightExp.getAttr(context, transform)
return if (left is CharSequence && right is CharSequence) {
!left.endsWith(
right
@ -206,13 +212,14 @@ sealed class CompareOperator(val key: String) : Stringify {
data object Less : CompareOperator("<") {
override fun <T> compare(
node: T,
context: Context<T>,
transform: Transform<T>,
leftExp: ValueExpression,
rightExp: ValueExpression
): Boolean {
val left = leftExp.getAttr(node, transform)
val right = rightExp.getAttr(node, transform)
rightExp: ValueExpression,
): Boolean {
val left = leftExp.getAttr(context, transform)
val right = rightExp.getAttr(context, transform)
return if (left is Int && right is Int) left < right else false
}
@ -224,13 +231,14 @@ sealed class CompareOperator(val key: String) : Stringify {
data object LessEqual : CompareOperator("<=") {
override fun <T> compare(
node: T,
context: Context<T>,
transform: Transform<T>,
leftExp: ValueExpression,
rightExp: ValueExpression
): Boolean {
val left = leftExp.getAttr(node, transform)
val right = rightExp.getAttr(node, transform)
rightExp: ValueExpression,
): Boolean {
val left = leftExp.getAttr(context, transform)
val right = rightExp.getAttr(context, transform)
return if (left is Int && right is Int) left <= right else false
}
@ -240,13 +248,14 @@ sealed class CompareOperator(val key: String) : Stringify {
data object More : CompareOperator(">") {
override fun <T> compare(
node: T,
context: Context<T>,
transform: Transform<T>,
leftExp: ValueExpression,
rightExp: ValueExpression
): Boolean {
val left = leftExp.getAttr(node, transform)
val right = rightExp.getAttr(node, transform)
rightExp: ValueExpression,
): Boolean {
val left = leftExp.getAttr(context, transform)
val right = rightExp.getAttr(context, transform)
return if (left is Int && right is Int) left > right else false
}
@ -256,13 +265,14 @@ sealed class CompareOperator(val key: String) : Stringify {
data object MoreEqual : CompareOperator(">=") {
override fun <T> compare(
node: T,
context: Context<T>,
transform: Transform<T>,
leftExp: ValueExpression,
rightExp: ValueExpression
): Boolean {
val left = leftExp.getAttr(node, transform)
val right = rightExp.getAttr(node, transform)
rightExp: ValueExpression,
): Boolean {
val left = leftExp.getAttr(context, transform)
val right = rightExp.getAttr(context, transform)
return if (left is Int && right is Int) left >= right else false
}
@ -273,12 +283,13 @@ sealed class CompareOperator(val key: String) : Stringify {
data object Matches : CompareOperator("~=") {
override fun <T> compare(
node: T,
context: Context<T>,
transform: Transform<T>,
leftExp: ValueExpression,
rightExp: ValueExpression
): Boolean {
val left = leftExp.getAttr(node, transform)
rightExp: ValueExpression,
): Boolean {
val left = leftExp.getAttr(context, transform)
return if (left is CharSequence && rightExp is ValueExpression.StringLiteral) {
rightExp.outMatches(left)
} else {
@ -293,12 +304,13 @@ sealed class CompareOperator(val key: String) : Stringify {
data object NotMatches : CompareOperator("!~=") {
override fun <T> compare(
node: T,
context: Context<T>,
transform: Transform<T>,
leftExp: ValueExpression,
rightExp: ValueExpression
): Boolean {
val left = leftExp.getAttr(node, transform)
rightExp: ValueExpression,
): Boolean {
val left = leftExp.getAttr(context, transform)
return if (left is CharSequence && rightExp is ValueExpression.StringLiteral) {
!rightExp.outMatches(left)
} else {

View File

@ -1,6 +1,9 @@
package li.songe.selector
sealed class ConnectExpression {
import kotlin.js.JsExport
@JsExport
sealed class ConnectExpression : Stringify {
abstract val minOffset: Int
abstract val maxOffset: Int?
abstract fun checkOffset(offset: Int): Boolean

View File

@ -1,5 +1,8 @@
package li.songe.selector
import kotlin.js.JsExport
@JsExport
sealed class ConnectOperator(val key: String) : Stringify {
override fun stringify() = key
@ -22,7 +25,7 @@ sealed class ConnectOperator(val key: String) : Stringify {
data object BeforeBrother : ConnectOperator("+") {
override fun <T> traversal(
node: T, transform: Transform<T>, connectExpression: ConnectExpression
) = transform.getBeforeBrothers(node, connectExpression)
) = transform.traverseBeforeBrothers(node, connectExpression)
}
@ -32,7 +35,7 @@ sealed class ConnectOperator(val key: String) : Stringify {
data object AfterBrother : ConnectOperator("-") {
override fun <T> traversal(
node: T, transform: Transform<T>, connectExpression: ConnectExpression
) = transform.getAfterBrothers(node, connectExpression)
) = transform.traverseAfterBrothers(node, connectExpression)
}
/**
@ -41,7 +44,7 @@ sealed class ConnectOperator(val key: String) : Stringify {
data object Ancestor : ConnectOperator(">") {
override fun <T> traversal(
node: T, transform: Transform<T>, connectExpression: ConnectExpression
) = transform.getAncestors(node, connectExpression)
) = transform.traverseAncestors(node, connectExpression)
}
@ -51,7 +54,7 @@ sealed class ConnectOperator(val key: String) : Stringify {
data object Child : ConnectOperator("<") {
override fun <T> traversal(
node: T, transform: Transform<T>, connectExpression: ConnectExpression
) = transform.getChildrenX(node, connectExpression)
) = transform.traverseChildren(node, connectExpression)
}
/**
@ -60,7 +63,7 @@ sealed class ConnectOperator(val key: String) : Stringify {
data object Descendant : ConnectOperator("<<") {
override fun <T> traversal(
node: T, transform: Transform<T>, connectExpression: ConnectExpression
) = transform.getDescendantsX(node, connectExpression)
) = transform.traverseDescendants(node, connectExpression)
}
}

View File

@ -1,17 +1,28 @@
package li.songe.selector
import kotlin.js.JsExport
@JsExport
data class ConnectSegment(
val operator: ConnectOperator = ConnectOperator.Ancestor,
val connectExpression: ConnectExpression = PolynomialExpression(),
) {
override fun toString(): String {
if (operator == ConnectOperator.Ancestor && connectExpression is PolynomialExpression && connectExpression.a == 1 && connectExpression.b == 0) {
) : Stringify {
override fun stringify(): String {
if (isMatchAnyAncestor) {
return ""
}
return operator.stringify() + connectExpression.toString()
return operator.stringify() + connectExpression.stringify()
}
internal fun <T> traversal(node: T, transform: Transform<T>): Sequence<T?> {
return operator.traversal(node, transform, connectExpression)
}
val isMatchAnyAncestor = operator == ConnectOperator.Ancestor
&& connectExpression is PolynomialExpression
&& connectExpression.a == 1 && connectExpression.b == 0
val isMatchAnyDescendant = operator == ConnectOperator.Descendant
&& connectExpression is PolynomialExpression
&& connectExpression.a == 1 && connectExpression.b == 0
}

View File

@ -1,22 +1,33 @@
package li.songe.selector
import kotlin.js.JsExport
@JsExport
data class ConnectWrapper(
val segment: ConnectSegment,
val to: PropertyWrapper,
) {
override fun toString(): String {
return (to.toString() + "\u0020" + segment.toString()).trim()
) : Stringify {
override fun stringify(): String {
return (to.stringify() + "\u0020" + segment.stringify()).trim()
}
internal fun <T> matchTracks(
node: T, transform: Transform<T>,
trackNodes: MutableList<T>,
): List<T>? {
segment.traversal(node, transform).forEach {
fun <T> matchContext(
context: Context<T>,
transform: Transform<T>,
): Context<T>? {
if (isMatchRoot) {
// C <<n [parent=null] >n A
val root = transform.getRoot(context.current) ?: return null
return to.matchContext(context.next(root), transform)
}
segment.traversal(context.current, transform).forEach {
if (it == null) return@forEach
val r = to.matchTracks(it, transform, trackNodes)
val r = to.matchContext(context.next(it), transform)
if (r != null) return r
}
return null
}
private val isMatchRoot = to.isMatchRoot && segment.isMatchAnyAncestor
private val canQf = to.quickFindValue.canQf && segment.isMatchAnyDescendant
}

View File

@ -0,0 +1,42 @@
package li.songe.selector
import kotlin.js.JsExport
@JsExport
data class Context<T>(
val current: T,
val prev: Context<T>? = null,
) {
fun getPrev(index: Int): Context<T>? {
if (index < 0) return null
var context = prev ?: return null
repeat(index) {
context = context.prev ?: return null
}
return context
}
fun get(index: Int): Context<T> {
if (index == 0) return this
return getPrev(index - 1) ?: throw IndexOutOfBoundsException()
}
fun toList(): List<T> {
val list = mutableListOf(this.current)
var context = prev
while (context != null) {
list.add(context.current)
context = context.prev
}
return list
}
@Suppress("UNCHECKED_CAST")
fun toArray(): Array<T> {
return (toList() as List<Any>).toTypedArray() as Array<T>
}
fun next(value: T): Context<T> {
return Context(value, this)
}
}

View File

@ -1,7 +1,13 @@
package li.songe.selector
import kotlin.js.JsExport
@JsExport
sealed class Expression : Position {
internal abstract fun <T> match(node: T, transform: Transform<T>): Boolean
abstract fun <T> match(
context: Context<T>,
transform: Transform<T>,
): Boolean
abstract val binaryExpressions: Array<BinaryExpression>
}

View File

@ -1,6 +1,8 @@
package li.songe.selector
import kotlin.js.JsExport
@JsExport
data class LogicalExpression(
override val start: Int,
override val end: Int,
@ -8,8 +10,11 @@ data class LogicalExpression(
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 fun <T> match(
context: Context<T>,
transform: Transform<T>,
): Boolean {
return operator.value.compare(context, transform, left, right)
}
override val binaryExpressions

View File

@ -1,5 +1,8 @@
package li.songe.selector
import kotlin.js.JsExport
@JsExport
sealed class LogicalOperator(val key: String) : Stringify {
override fun stringify() = key
@ -12,8 +15,8 @@ sealed class LogicalOperator(val key: String) : Stringify {
}
}
internal abstract fun <T> compare(
node: T,
abstract fun <T> compare(
context: Context<T>,
transform: Transform<T>,
left: Expression,
right: Expression,
@ -21,23 +24,29 @@ sealed class LogicalOperator(val key: String) : Stringify {
data object AndOperator : LogicalOperator("&&") {
override fun <T> compare(
node: T,
context: Context<T>,
transform: Transform<T>,
left: Expression,
right: Expression,
): Boolean {
return left.match(node, transform) && right.match(node, transform)
return left.match(context, transform) && right.match(
context,
transform
)
}
}
data object OrOperator : LogicalOperator("||") {
override fun <T> compare(
node: T,
context: Context<T>,
transform: Transform<T>,
left: Expression,
right: Expression,
): Boolean {
return left.match(node, transform) || right.match(node, transform)
return left.match(context, transform) || right.match(
context,
transform
)
}
}
}

View File

@ -1,35 +0,0 @@
package li.songe.selector
import kotlin.js.JsExport
@JsExport
@Suppress("UNUSED", "UNCHECKED_CAST")
class MultiplatformSelector private constructor(
internal val selector: Selector,
) {
val tracks = selector.tracks
val trackIndex = selector.trackIndex
val connectKeys = selector.connectKeys
val isMatchRoot = selector.isMatchRoot
val quickFindValue = selector.quickFindValue
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)
}
fun <T : Any> matchTrack(node: T, transform: MultiplatformTransform<T>): Array<T>? {
return selector.matchTracks(node, transform.transform)?.toTypedArray<Any?>() as Array<T>?
}
override fun toString() = selector.toString()
companion object {
fun parse(source: String) = MultiplatformSelector(Selector.parse(source))
fun parseOrNull(source: String) = Selector.parseOrNull(source)?.let(::MultiplatformSelector)
}
}

View File

@ -1,43 +0,0 @@
package li.songe.selector
import kotlin.js.JsExport
@JsExport
@Suppress("UNCHECKED_CAST", "UNUSED")
class MultiplatformTransform<T : 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,
)
val querySelectorAll: (T, MultiplatformSelector) -> Array<T> = { node, selector ->
val result =
transform.querySelectorAll(node, selector.selector).toList().toTypedArray<Any?>()
result as Array<T>
}
val querySelector: (T, MultiplatformSelector) -> T? = { node, selector ->
transform.querySelectorAll(node, selector.selector).firstOrNull()
}
val querySelectorTrackAll: (T, MultiplatformSelector) -> Array<Array<T>> = { node, selector ->
val result = transform.querySelectorTrackAll(node, selector.selector)
.map { it.toTypedArray<Any?>() as Array<T> }.toList().toTypedArray<Any?>()
result as Array<Array<T>>
}
val querySelectorTrack: (T, MultiplatformSelector) -> Array<T>? = { node, selector ->
transform.querySelectorTrackAll(node, selector.selector).firstOrNull()
?.toTypedArray<Any?>() as Array<T>?
}
}

View File

@ -1,5 +1,8 @@
package li.songe.selector
import kotlin.js.JsExport
@JsExport
data class NotExpression(
override val start: Int,
val expression: Expression
@ -7,8 +10,11 @@ data class NotExpression(
override val end: Int
get() = expression.end
override fun <T> match(node: T, transform: Transform<T>): Boolean {
return !expression.match(node, transform)
override fun <T> match(
context: Context<T>,
transform: Transform<T>,
): Boolean {
return !expression.match(context, transform)
}
override val binaryExpressions: Array<BinaryExpression>

View File

@ -1,11 +1,14 @@
package li.songe.selector
import kotlin.js.JsExport
/**
* an+b
*/
@JsExport
data class PolynomialExpression(val a: Int = 0, val b: Int = 1) : ConnectExpression() {
override fun toString(): String {
override fun stringify(): String {
if (a > 0 && b > 0) {
if (a == 1) {
return "(n+$b)"

View File

@ -1,21 +1,20 @@
package li.songe.selector
import kotlin.js.JsExport
@JsExport
data class PropertySegment(
/**
* 此属性选择器是否被 @ 标记
*/
val tracked: Boolean,
val at: Boolean,
val name: String,
val expressions: List<Expression>,
) {
) : Stringify {
private val matchAnyName = name.isBlank() || name == "*"
val binaryExpressions
get() = expressions.flatMap { it.binaryExpressions.toList() }.toTypedArray()
override fun toString(): String {
val matchTag = if (tracked) "@" else ""
override fun stringify(): String {
val matchTag = if (at) "@" else ""
return matchTag + name + expressions.joinToString("") { "[${it.stringify()}]" }
}
@ -30,8 +29,16 @@ data class PropertySegment(
return false
}
fun <T> match(node: T, transform: Transform<T>): Boolean {
return matchName(node, transform) && expressions.all { ex -> ex.match(node, transform) }
fun <T> match(
context: Context<T>,
transform: Transform<T>,
): Boolean {
return matchName(context.current, transform) && expressions.all { ex ->
ex.match(
context,
transform
)
}
}
}

View File

@ -1,33 +1,40 @@
package li.songe.selector
import kotlin.js.JsExport
@JsExport
data class PropertyWrapper(
val segment: PropertySegment,
val to: ConnectWrapper? = null,
) {
override fun toString(): String {
):Stringify {
override fun stringify(): String {
return (if (to != null) {
to.toString() + "\u0020"
to.stringify() + "\u0020"
} else {
""
}) + segment.toString()
}) + segment.stringify()
}
fun <T> matchTracks(
node: T,
fun <T> matchContext(
context: Context<T>,
transform: Transform<T>,
trackNodes: MutableList<T>,
): List<T>? {
if (!segment.match(node, transform)) {
): Context<T>? {
if (!segment.match(context, transform)) {
return null
}
trackNodes.add(node)
if (to == null) {
return trackNodes
return context
}
val r = to.matchTracks(node, transform, trackNodes)
if (r == null) {
trackNodes.removeLast()
}
return r
return to.matchContext(context, transform)
}
val isMatchRoot = segment.expressions.any { e ->
e is BinaryExpression && e.operator.value == CompareOperator.Equal && ((e.left.value == "depth" && e.right.value == 0) || (e.left.value == "parent" && e.right.value == "null"))
}
val quickFindValue = getQuickFindValue(segment)
val length: Int
get() = if (to == null) 1 else to.to.length + 1
}

View File

@ -8,8 +8,7 @@ data class QuickFindValue(
val vid: String?,
val text: String?,
) {
val canQf: Boolean
get() = id != null || vid != null || text != null
val canQf = id != null || vid != null || text != null
}
internal fun getQuickFindValue(segment: PropertySegment): QuickFindValue {

View File

@ -1,50 +1,49 @@
package li.songe.selector
import li.songe.selector.parser.selectorParser
import kotlin.js.JsExport
class Selector internal constructor(
@JsExport
class Selector(
val source: String,
private val propertyWrapper: PropertyWrapper
) {
override fun toString(): String {
return propertyWrapper.toString()
val propertyWrapper: PropertyWrapper
) : Stringify {
override fun stringify(): String {
return propertyWrapper.stringify()
}
val tracks = run {
val list = mutableListOf(propertyWrapper)
while (true) {
list.add(list.last().to?.to ?: break)
val targetIndex = run {
val length = propertyWrapper.length
var index = 0
var c: PropertyWrapper? = propertyWrapper
while (c != null) {
if (c.segment.at) {
return@run length - 1 - index
}
c = c.to?.to
index++
}
list.map { p -> p.segment.tracked }.toTypedArray<Boolean>()
length - 1
}
val trackIndex = tracks.indexOfFirst { it }.let { i ->
if (i < 0) 0 else i
fun <T> matchContext(
node: T,
transform: Transform<T>,
): Context<T>? {
return propertyWrapper.matchContext(Context(node), transform)
}
fun <T> match(
node: T,
transform: Transform<T>,
trackNodes: MutableList<T> = ArrayList(tracks.size),
): T? {
val trackTempNodes = matchTracks(node, transform, trackNodes) ?: return null
return trackTempNodes[trackIndex]
val ctx = matchContext(node, transform) ?: return null
return ctx.get(targetIndex).current
}
fun <T> matchTracks(
node: T,
transform: Transform<T>,
trackNodes: MutableList<T> = ArrayList(tracks.size),
): List<T>? {
return propertyWrapper.matchTracks(node, transform, trackNodes)
}
val quickFindValue = propertyWrapper.quickFindValue
val quickFindValue = getQuickFindValue(propertyWrapper.segment)
// 主动查询根节点
val isMatchRoot = propertyWrapper.segment.expressions.any { e ->
e is BinaryExpression && e.operator.value == CompareOperator.Equal && ((e.left.value == "depth" && e.right.value == 0) || (e.left.value == "parent" && e.right.value == "null"))
}
val isMatchRoot = propertyWrapper.isMatchRoot
val connectKeys = run {
var c = propertyWrapper.to

View File

@ -1,13 +1,22 @@
package li.songe.selector
@Suppress("UNUSED")
class Transform<T>(
val getAttr: (Any?, String) -> Any?,
val getInvoke: (Any?, String, List<Any>) -> Any? = { _, _, _ -> null },
import kotlin.js.JsExport
@JsExport
class Transform<T> @JsExport.Ignore constructor(
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?,
val getRoot: (T) -> T? = { node ->
var parentVar: T? = getParent(node)
while (parentVar != null) {
parentVar = getParent(parentVar!!)
}
parentVar
},
val getDescendants: (T) -> Sequence<T> = { node ->
sequence { // 深度优先 先序遍历
// https://developer.mozilla.org/zh-CN/docs/Web/API/Document/querySelector
@ -16,7 +25,7 @@ class Transform<T>(
stack.reverse()
val tempNodes = mutableListOf<T>()
do {
val top = stack.removeLast()
val top = stack.removeAt(stack.lastIndex)
yield(top)
for (childNode in getChildren(top)) {
// 可针对 sequence 优化
@ -32,22 +41,20 @@ class Transform<T>(
}
},
val getChildrenX: (T, ConnectExpression) -> Sequence<T> = { node, connectExpression ->
getChildren(node)
.let {
if (connectExpression.maxOffset != null) {
it.take(connectExpression.maxOffset!! + 1)
} else {
it
}
}
.filterIndexed { i, _ ->
connectExpression.checkOffset(
i
)
val traverseChildren: (T, ConnectExpression) -> Sequence<T> = { node, connectExpression ->
getChildren(node).let {
if (connectExpression.maxOffset != null) {
it.take(connectExpression.maxOffset!! + 1)
} else {
it
}
}.filterIndexed { i, _ ->
connectExpression.checkOffset(
i
)
}
},
val getAncestors: (T, ConnectExpression) -> Sequence<T> = { node, connectExpression ->
val traverseAncestors: (T, ConnectExpression) -> Sequence<T> = { node, connectExpression ->
sequence {
var parentVar: T? = getParent(node) ?: return@sequence
var offset = 0
@ -67,7 +74,7 @@ class Transform<T>(
}
}
},
val getBeforeBrothers: (T, ConnectExpression) -> Sequence<T> = { node, connectExpression ->
val traverseBeforeBrothers: (T, ConnectExpression) -> Sequence<T> = { node, connectExpression ->
val parentVal = getParent(node)
if (parentVal != null) {
val list = getChildren(parentVal).takeWhile { it != node }.toMutableList()
@ -81,28 +88,25 @@ class Transform<T>(
emptySequence()
}
},
val getAfterBrothers: (T, ConnectExpression) -> Sequence<T> = { node, connectExpression ->
val traverseAfterBrothers: (T, ConnectExpression) -> Sequence<T> = { node, connectExpression ->
val parentVal = getParent(node)
if (parentVal != null) {
getChildren(parentVal).dropWhile { it != node }
.drop(1)
.let {
if (connectExpression.maxOffset != null) {
it.take(connectExpression.maxOffset!! + 1)
} else {
it
}
}.filterIndexed { i, _ ->
connectExpression.checkOffset(
i
)
getChildren(parentVal).dropWhile { it != node }.drop(1).let {
if (connectExpression.maxOffset != null) {
it.take(connectExpression.maxOffset!! + 1)
} else {
it
}
}.filterIndexed { i, _ ->
connectExpression.checkOffset(
i
)
}
} else {
emptySequence()
}
},
val getDescendantsX: (T, ConnectExpression) -> Sequence<T> = { node, connectExpression ->
val traverseDescendants: (T, ConnectExpression) -> Sequence<T> = { node, connectExpression ->
sequence {
val stack = getChildren(node).toMutableList()
if (stack.isEmpty()) return@sequence
@ -110,7 +114,7 @@ class Transform<T>(
val tempNodes = mutableListOf<T>()
var offset = 0
do {
val top = stack.removeLast()
val top = stack.removeAt(stack.lastIndex)
if (connectExpression.checkOffset(offset)) {
yield(top)
}
@ -132,40 +136,60 @@ class Transform<T>(
} while (stack.isNotEmpty())
}
},
) {
) {
@JsExport.Ignore
val querySelectorAll: (T, Selector) -> Sequence<T> = { node, selector ->
sequence {
// cache trackNodes
val trackNodes = ArrayList<T>(selector.tracks.size)
val r0 = selector.match(node, this@Transform, trackNodes)
if (r0 != null) yield(r0)
selector.match(node, this@Transform)?.let { yield(it) }
getDescendants(node).forEach { childNode ->
trackNodes.clear()
val r = selector.match(childNode, this@Transform, trackNodes)
if (r != null) yield(r)
selector.match(childNode, this@Transform)?.let { yield(it) }
}
}
}
val querySelector: (T, Selector) -> T? = { node, selector ->
querySelectorAll(
node, selector
).firstOrNull()
querySelectorAll(node, selector).firstOrNull()
}
val querySelectorTrackAll: (T, Selector) -> Sequence<List<T>> = { node, selector ->
@JsExport.Ignore
val querySelectorAllContext: (T, Selector) -> Sequence<Context<T>> = { node, selector ->
sequence {
val r0 = selector.matchTracks(node, this@Transform)
if (r0 != null) yield(r0)
selector.matchContext(node, this@Transform)?.let { yield(it) }
getDescendants(node).forEach { childNode ->
val r = selector.matchTracks(childNode, this@Transform)
if (r != null) yield(r)
selector.matchContext(childNode, this@Transform)?.let { yield(it) }
}
}
}
val querySelectorTrack: (T, Selector) -> List<T>? = { node, selector ->
querySelectorTrackAll(
node, selector
).firstOrNull()
val querySelectorContext: (T, Selector) -> Context<T>? = { node, selector ->
querySelectorAllContext(node, selector).firstOrNull()
}
@Suppress("UNCHECKED_CAST")
val querySelectorAllArray: (T, Selector) -> Array<T> = { node, selector ->
val result = querySelectorAll(node, selector).toList()
(result as List<Any>).toTypedArray() as Array<T>
}
val querySelectorAllContextArray: (T, Selector) -> Array<Context<T>> = { node, selector ->
val result = querySelectorAllContext(node, selector)
result.toList().toTypedArray()
}
companion object {
fun <T> multiplatformBuild(
getAttr: (Any, String) -> Any?,
getInvoke: (Any, String, List<Any>) -> Any?,
getName: (T) -> String?,
getChildren: (T) -> Array<T>,
getParent: (T) -> T?,
): Transform<T> {
return Transform(
getAttr = getAttr,
getInvoke = { target, name, args -> getInvoke(target, name, args) },
getName = getName,
getChildren = { node -> getChildren(node).asSequence() },
getParent = getParent,
)
}
}
}

View File

@ -1,5 +1,8 @@
package li.songe.selector
import kotlin.js.JsExport
@JsExport
data class TupleExpression(
val numbers: List<Int>,
) : ConnectExpression() {
@ -15,7 +18,7 @@ data class TupleExpression(
return numbers[i]
}
override fun toString(): String {
override fun stringify(): String {
if (numbers.size == 1) {
return if (numbers.first() == 1) {
""

View File

@ -5,7 +5,11 @@ 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?
internal abstract fun <T> getAttr(
context: Context<T>,
transform: Transform<T>,
): Any?
abstract val properties: Array<String>
abstract val methods: Array<String>
@ -18,8 +22,8 @@ sealed class ValueExpression(open val value: Any?, open val type: String) : Posi
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 fun <T> getAttr(context: Context<T>, transform: Transform<T>): Any? {
return transform.getAttr(context, value)
}
override val properties: Array<String>
@ -34,8 +38,14 @@ sealed class ValueExpression(open val value: Any?, open val type: String) : Posi
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 fun <T> getAttr(
context: Context<T>,
transform: Transform<T>,
): Any? {
return transform.getAttr(
object0.getAttr(context, transform).whenNull { return null },
property
)
}
override val properties: Array<String>
@ -53,7 +63,10 @@ sealed class ValueExpression(open val value: Any?, open val type: String) : Posi
value = "${callee.stringify()}(${arguments.joinToString(",") { it.stringify() }})",
) {
override fun <T> getAttr(node: T, transform: Transform<T>): Any? {
override fun <T> getAttr(
context: Context<T>,
transform: Transform<T>,
): Any? {
return when (callee) {
is CallExpression -> {
// not support
@ -62,17 +75,21 @@ sealed class ValueExpression(open val value: Any?, open val type: String) : Posi
is Identifier -> {
transform.getInvoke(
node,
context,
callee.value,
arguments.map { it.getAttr(node, transform).whenNull { return null } }
arguments.map {
it.getAttr(context, transform).whenNull { return null }
}
)
}
is MemberExpression -> {
transform.getInvoke(
callee.object0.getAttr(node, transform).whenNull { return null },
callee.object0.getAttr(context, transform).whenNull { return null },
callee.property,
arguments.map { it.getAttr(node, transform).whenNull { return null } }
arguments.map {
it.getAttr(context, transform).whenNull { return null }
}
)
}
}
@ -95,7 +112,10 @@ sealed class ValueExpression(open val value: Any?, open val type: String) : Posi
override val value: Any?,
override val type: String,
) : ValueExpression(value, type) {
override fun <T> getAttr(node: T, transform: Transform<T>) = value
override fun <T> getAttr(
context: Context<T>,
transform: Transform<T>,
) = value
override val properties: Array<String>
get() = emptyArray()

View File

@ -2,6 +2,7 @@ package li.songe.selector
import kotlin.js.JsExport
expect fun String.toMatches(): (input: CharSequence) -> Boolean
expect fun setWasmToMatches(wasmToMatches: (String) -> (String) -> Boolean)

View File

@ -148,8 +148,14 @@ fun initDefaultTypeInfo(): DefaultTypeInfo {
nodeType.methods = arrayOf(
MethodInfo("getChild", nodeType, arrayOf(intType)),
)
contextType.methods = arrayOf(*nodeType.methods)
contextType.props = arrayOf(*nodeType.props)
contextType.methods = arrayOf(
*nodeType.methods,
MethodInfo("getPrev", contextType, arrayOf(intType))
)
contextType.props = arrayOf(
*nodeType.props,
PropInfo("prev", contextType),
)
return DefaultTypeInfo(
booleanType = booleanType,
intType = intType,
@ -160,7 +166,7 @@ fun initDefaultTypeInfo(): DefaultTypeInfo {
}
@JsExport
fun getIntInvoke(target: Int, name: String, args: List<Any?>): Any? {
fun getIntInvoke(target: Int, name: String, args: List<Any>): Any? {
return when (name) {
"plus" -> {
target + (args.getIntOrNull() ?: return null)
@ -187,26 +193,26 @@ fun getIntInvoke(target: Int, name: String, args: List<Any?>): Any? {
}
internal fun List<Any?>.getIntOrNull(i: Int = 0): Int? {
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? {
fun getStringInvoke(target: String, name: String, args: List<Any>): Any? {
return getCharSequenceInvoke(target, name, args)
}
@JsExport
fun getBooleanInvoke(target: Boolean, name: String, args: List<Any?>): Any? {
fun getBooleanInvoke(target: Boolean, name: String, args: List<Any>): Any? {
return when (name) {
"toInt" -> if (target) 1 else 0
else -> null
}
}
fun getCharSequenceInvoke(target: CharSequence, name: String, args: List<Any?>): Any? {
fun getCharSequenceInvoke(target: CharSequence, name: String, args: List<Any>): Any? {
return when (name) {
"get" -> {
target.getOrNull(args.getIntOrNull() ?: return null).toString()

View File

@ -34,7 +34,7 @@ class ParserTest {
return value.intOrNull ?: value.booleanOrNull ?: value.content
}
private fun getNodeInvoke(target: TestNode, name: String, args: List<Any?>): Any? {
private fun getNodeInvoke(target: TestNode, name: String, args: List<Any>): Any? {
when (name) {
"getChild" -> {
val arg = (args.getIntOrNull() ?: return null)
@ -47,6 +47,11 @@ class ParserTest {
private val transform = Transform<TestNode>(
getAttr = { target, name ->
when (target) {
is Context<*> -> when (name) {
"prev" -> target.prev
else -> getNodeAttr(target.current as TestNode, name)
}
is TestNode -> getNodeAttr(target, name)
is String -> getCharSequenceAttr(target, name)
@ -59,6 +64,14 @@ class ParserTest {
is Int -> getIntInvoke(target, name, args)
is CharSequence -> getCharSequenceInvoke(target, name, args)
is TestNode -> getNodeInvoke(target, name, args)
is Context<*> -> when (name) {
"getPrev" -> {
args.getIntOrNull()?.let { target.getPrev(it) }
}
else -> getNodeInvoke(target.current as TestNode, name, args)
}
else -> null
}
},
@ -121,7 +134,7 @@ class ParserTest {
val text =
"ImageView < @FrameLayout < LinearLayout < RelativeLayout <n LinearLayout < RelativeLayout + LinearLayout > RelativeLayout > TextView[text\$='广告']"
val selector = Selector.parse(text)
println("trackIndex: " + selector.trackIndex)
println("trackIndex: " + selector.targetIndex)
println("canCacheIndex: " + Selector.parse("A + B").useCache)
println("canCacheIndex: " + Selector.parse("A > B - C").useCache)
}
@ -139,12 +152,10 @@ class ParserTest {
assertTrue(targets.size == 1)
println("id: " + targets.first().id)
val trackTargets = transform.querySelectorTrackAll(node, selector).toList()
val trackTargets = transform.querySelectorAllContext(node, selector).toList()
println("trackTargets_size: " + trackTargets.size)
assertTrue(trackTargets.size == 1)
println(trackTargets.first().mapIndexed { index, testNode ->
testNode.id to selector.tracks[index]
})
println(trackTargets.first())
}
@Test
@ -157,11 +168,10 @@ class ParserTest {
@Test
fun check_query() {
val text = "@TextView - [text=\"签到提醒\"] <<n [vid=\"webViewContainer\"]"
val text = "@TextView[getPrev(0).text=`签到提醒`] - [text=`签到提醒`] <<n [vid=`webViewContainer`]"
val selector = Selector.parse(text)
println("selector: $selector")
println(selector.trackIndex)
println(selector.tracks.toList())
println(selector.targetIndex)
val node = getOrDownloadNode("https://i.gkd.li/i/14384152")
val targets = transform.querySelectorAll(node, selector).toList()
@ -249,7 +259,7 @@ class ParserTest {
@Test
fun check_type() {
val source =
"[visibleToUser=true][((parent.getChild(0,).getChild( (0), )=null) && (((`` >= 1)))) || (name=null && desc=null)]"
"[prev!=null&&visibleToUser=true][((parent.getChild(0,).getChild( (0), )=null) && (((2 >= 1)))) || (name=null && desc=null)]"
val selector = Selector.parse(source)
val typeInfo = initDefaultTypeInfo().contextType
val error = selector.checkType(typeInfo)