mirror of
https://github.com/gkd-kit/gkd.git
synced 2024-11-16 11:42:22 +08:00
feat(selector): support not exp, method invoke, type check, object type
Some checks are pending
Build-Apk / build (push) Waiting to run
Some checks are pending
Build-Apk / build (push) Waiting to run
This commit is contained in:
parent
695563c7b8
commit
6490018230
|
@ -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) {
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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 },
|
||||
|
|
|
@ -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" }
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
package li.songe.selector.data
|
||||
package li.songe.selector
|
||||
|
||||
sealed class ConnectExpression {
|
||||
abstract val minOffset: Int
|
|
@ -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>
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
|
@ -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()}")
|
|
@ -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>
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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,
|
|
@ -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)
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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()})"
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
package li.songe.selector.data
|
||||
package li.songe.selector
|
||||
|
||||
/**
|
||||
* an+b
|
26
selector/src/commonMain/kotlin/li/songe/selector/Position.kt
Normal file
26
selector/src/commonMain/kotlin/li/songe/selector/Position.kt
Normal 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()
|
||||
}
|
||||
}
|
|
@ -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 {
|
|
@ -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)
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -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?,
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
package li.songe.selector.data
|
||||
package li.songe.selector
|
||||
|
||||
data class TupleExpression(
|
||||
val numbers: List<Int>,
|
71
selector/src/commonMain/kotlin/li/songe/selector/TypeInfo.kt
Normal file
71
selector/src/commonMain/kotlin/li/songe/selector/TypeInfo.kt
Normal 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
|
||||
}
|
||||
}
|
|
@ -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 }
|
||||
}
|
||||
}
|
|
@ -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}"
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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())
|
||||
}
|
280
selector/src/commonMain/kotlin/li/songe/selector/util.kt
Normal file
280
selector/src/commonMain/kotlin/li/songe/selector/util.kt
Normal 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
|
||||
}
|
||||
}
|
|
@ -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")
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user