perf: indexCache

This commit is contained in:
lisonge 2024-02-26 00:09:13 +08:00
parent 36719e2b5f
commit ad7d39a042
35 changed files with 888 additions and 509 deletions

2
.gitignore vendored
View File

@ -19,3 +19,5 @@ local.properties
*.jks
*.keystore
/_assets

View File

@ -3,6 +3,8 @@ package li.songe.gkd.data
import android.accessibilityservice.AccessibilityService
import android.view.accessibility.AccessibilityNodeInfo
import kotlinx.coroutines.Job
import li.songe.gkd.service.CacheTransform
import li.songe.gkd.service.createCacheTransform
import li.songe.gkd.service.lastTriggerRule
import li.songe.gkd.service.lastTriggerTime
import li.songe.gkd.service.querySelector
@ -119,15 +121,37 @@ sealed class ResolvedRule(
else -> true
}
fun query(nodeInfo: AccessibilityNodeInfo?): AccessibilityNodeInfo? {
private val canCacheIndex = (matches + excludeMatches).any { s -> s.canCacheIndex }
fun query(
nodeInfo: AccessibilityNodeInfo?,
cacheTransform: CacheTransform? = null
): AccessibilityNodeInfo? {
if (nodeInfo == null) return null
var target: AccessibilityNodeInfo? = null
for (selector in matches) {
target = nodeInfo.querySelector(selector, quickFind) ?: return null
}
for (selector in excludeMatches) {
if (nodeInfo.querySelector(selector, quickFind) != null) return null
if (canCacheIndex) {
val transform = cacheTransform ?: createCacheTransform()
for (selector in matches) {
target = nodeInfo.querySelector(selector, quickFind, transform.transform)
?: return null
}
for (selector in excludeMatches) {
if (nodeInfo.querySelector(
selector,
quickFind,
transform.transform
) != null
) return null
}
} else {
for (selector in matches) {
target = nodeInfo.querySelector(selector, quickFind) ?: return null
}
for (selector in excludeMatches) {
if (nodeInfo.querySelector(selector, quickFind) != null) return null
}
}
return target
}

View File

@ -5,6 +5,7 @@ import com.blankj.utilcode.util.LogUtils
import io.ktor.http.HttpStatusCode
import io.ktor.server.application.createApplicationPlugin
import io.ktor.server.application.hooks.CallFailed
import io.ktor.server.plugins.origin
import io.ktor.server.request.uri
import io.ktor.server.response.respond
import li.songe.gkd.data.RpcError
@ -13,7 +14,7 @@ val KtorErrorPlugin = createApplicationPlugin(name = "KtorErrorPlugin") {
onCall { call ->
// TODO 在局域网会被扫描工具批量请求多个路径
if (call.request.uri == "/" || call.request.uri.startsWith("/api/")) {
Log.d("Ktor", "onCall: ${call.request.uri}")
Log.d("Ktor", "onCall: ${call.request.origin.remoteAddress} -> ${call.request.uri}")
}
}
on(CallFailed) { call, cause ->

View File

@ -56,14 +56,27 @@ fun AccessibilityNodeInfo.getDepth(): Int {
return depth
}
fun AccessibilityNodeInfo.getVid(): CharSequence? {
val id = viewIdResourceName ?: return null
val appId = packageName ?: return null
if (id.startsWith(appId) && id.startsWith(":id/", appId.length)) {
return id.subSequence(
appId.length + ":id/".length,
id.length
)
}
return null
}
fun AccessibilityNodeInfo.querySelector(
selector: Selector,
quickFind: Boolean = false,
transform: Transform<AccessibilityNodeInfo>? = null,
): AccessibilityNodeInfo? {
val t = (if (selector.canCacheIndex) transform else defaultTransform) ?: defaultTransform
if (selector.isMatchRoot) {
if (parent == null) {
val trackNodes = mutableListOf<AccessibilityNodeInfo>()
return selector.match(this, abTransform, trackNodes)
return selector.match(this, t)
}
return null
}
@ -81,16 +94,16 @@ fun AccessibilityNodeInfo.querySelector(
emptyList()
})
if (nodes.isNotEmpty()) {
val trackNodes = mutableListOf<AccessibilityNodeInfo>()
val trackNodes = ArrayList<AccessibilityNodeInfo>(selector.tracks.size)
nodes.forEach { childNode ->
val targetNode = selector.match(childNode, abTransform, trackNodes)
val targetNode = selector.match(childNode, t, trackNodes)
if (targetNode != null) return targetNode
}
}
return null
}
// 在一些开屏广告的界面会造成1-2s的阻塞
return abTransform.querySelector(this, selector)
return t.querySelector(this, selector)
}
// 不可以在 多线程/不同协程作用域 里同时使用
@ -148,17 +161,7 @@ val allowPropertyNames = setOf(
private val getAttr: (AccessibilityNodeInfo, String) -> Any? = { node, name ->
when (name) {
"id" -> node.viewIdResourceName
"vid" -> node.viewIdResourceName?.let { id ->
val appId = node.packageName
if (appId != null && id.startsWith(appId) && id.startsWith(":id/", appId.length)) {
id.subSequence(
appId.length + ":id/".length,
id.length
)
} else {
null
}
}
"vid" -> node.getVid()
"name" -> node.className
"text" -> node.text
@ -189,11 +192,194 @@ private val getAttr: (AccessibilityNodeInfo, String) -> Any? = { node, name ->
}
}
val abTransform = Transform(
data class CacheTransform(
val transform: Transform<AccessibilityNodeInfo>,
val indexCache: HashMap<AccessibilityNodeInfo, Int>,
)
fun createCacheTransform(): CacheTransform {
val indexCache = HashMap<AccessibilityNodeInfo, Int>()
fun AccessibilityNodeInfo.getChildX(index: Int): AccessibilityNodeInfo? {
return getChild(index)?.also { child ->
indexCache[child] = index
}
}
fun AccessibilityNodeInfo.getIndexX(): Int {
indexCache[this]?.let { return it }
parent?.forEachIndexed { index, child ->
if (child != null) {
indexCache[child] = index
}
if (child == this) {
return index
}
}
return 0
}
val getChildrenCache: (AccessibilityNodeInfo) -> Sequence<AccessibilityNodeInfo> = { node ->
sequence {
repeat(node.childCount.coerceAtMost(MAX_CHILD_SIZE)) { index ->
val child = node.getChildX(index) ?: return@sequence
yield(child)
}
}
}
val transform = Transform(
getAttr = { node, name ->
if (name == "index") {
node.getIndexX()
} else {
getAttr(node, name)
}
},
getName = { node -> node.className },
getChildren = getChildrenCache,
getParent = { node -> node.parent },
getDescendants = { node ->
sequence {
val stack = getChildrenCache(node).toMutableList()
if (stack.isEmpty()) return@sequence
stack.reverse()
val tempNodes = mutableListOf<AccessibilityNodeInfo>()
do {
val top = stack.removeLast()
yield(top)
for (childNode in getChildrenCache(top)) {
tempNodes.add(childNode)
}
if (tempNodes.isNotEmpty()) {
for (i in tempNodes.size - 1 downTo 0) {
stack.add(tempNodes[i])
}
tempNodes.clear()
}
} while (stack.isNotEmpty())
}.take(MAX_DESCENDANTS_SIZE)
},
getChildrenX = { node, connectExpression ->
sequence {
repeat(node.childCount.coerceAtMost(MAX_CHILD_SIZE)) { offset ->
connectExpression.maxOffset?.let { maxOffset ->
if (offset > maxOffset) return@sequence
}
if (connectExpression.checkOffset(offset)) {
val child = node.getChildX(offset) ?: return@sequence
yield(child)
}
}
}
},
getBeforeBrothers = { node, connectExpression ->
sequence {
val parentVal = node.parent ?: return@sequence
val index = indexCache[node] // 如果 node 由 quickFind 得到, 则第一次调用此方法可能得到 indexCache 是空
if (index != null) {
var i = index - 1
var offset = 0
while (0 <= i && i < parentVal.childCount) {
connectExpression.maxOffset?.let { maxOffset ->
if (offset > maxOffset) return@sequence
}
if (connectExpression.checkOffset(offset)) {
val child = parentVal.getChild(i) ?: return@sequence
yield(child)
}
i--
offset++
}
} else {
val list = getChildrenCache(parentVal).takeWhile { it != node }.toMutableList()
list.reverse()
yieldAll(list.filterIndexed { i, _ ->
connectExpression.checkOffset(
i
)
})
}
}
},
getAfterBrothers = { node, connectExpression ->
val parentVal = node.parent
if (parentVal != null) {
val index = indexCache[node]
if (index != null) {
sequence {
var i = index + 1
var offset = 0
while (0 <= i && i < parentVal.childCount) {
connectExpression.maxOffset?.let { maxOffset ->
if (offset > maxOffset) return@sequence
}
if (connectExpression.checkOffset(offset)) {
val child = parentVal.getChild(i) ?: return@sequence
yield(child)
}
i--
offset++
}
}
} else {
getChildrenCache(parentVal).dropWhile { it != node }
.drop(1)
.let {
if (connectExpression.maxOffset != null) {
it.take(connectExpression.maxOffset!! + 1)
} else {
it
}
}
.filterIndexed { i, _ ->
connectExpression.checkOffset(
i
)
}
}
} else {
emptySequence()
}
},
getDescendantsX = { node, connectExpression ->
sequence {
val stack = getChildrenCache(node).toMutableList()
if (stack.isEmpty()) return@sequence
stack.reverse()
val tempNodes = mutableListOf<AccessibilityNodeInfo>()
var offset = 0
do {
val top = stack.removeLast()
if (connectExpression.checkOffset(offset)) {
yield(top)
}
offset++
connectExpression.maxOffset?.let { maxOffset ->
if (offset > maxOffset) return@sequence
}
for (childNode in getChildrenCache(top)) {
tempNodes.add(childNode)
}
if (tempNodes.isNotEmpty()) {
for (i in tempNodes.size - 1 downTo 0) {
stack.add(tempNodes[i])
}
tempNodes.clear()
}
} while (stack.isNotEmpty())
}
},
)
return CacheTransform(transform, indexCache)
}
val defaultCacheTransform = createCacheTransform()
// no cache
val defaultTransform = Transform(
getAttr = getAttr,
getName = { node -> node.className },
getChildren = getChildren,
getChild = { node, index -> if (index in 0..<node.childCount) node.getChild(index) else null },
getParent = { node -> node.parent },
getDescendants = { node ->
sequence {
@ -201,9 +387,14 @@ val abTransform = Transform(
if (stack.isEmpty()) return@sequence
stack.reverse()
val tempNodes = mutableListOf<AccessibilityNodeInfo>()
var offset = 0
do {
val top = stack.removeLast()
yield(top)
offset++
if (offset > MAX_DESCENDANTS_SIZE) {
return@sequence
}
for (childNode in getChildren(top)) {
tempNodes.add(childNode)
}
@ -214,6 +405,19 @@ val abTransform = Transform(
tempNodes.clear()
}
} while (stack.isNotEmpty())
}.take(MAX_DESCENDANTS_SIZE)
}
}
},
getChildrenX = { node, connectExpression ->
sequence {
repeat(node.childCount.coerceAtMost(MAX_CHILD_SIZE)) { offset ->
connectExpression.maxOffset?.let { maxOffset ->
if (offset > maxOffset) return@sequence
}
if (connectExpression.checkOffset(offset)) {
val child = node.getChild(offset) ?: return@sequence
yield(child)
}
}
}
},
)

View File

@ -148,6 +148,7 @@ class GkdAbService : CompositionAbService({
} else {
queryThread
}
val common = ctx === queryThread
queryTaskJob = scope.launchTry(ctx) {
val activityRule = getAndUpdateCurrentRules()
for (rule in (activityRule.currentRules)) {
@ -180,7 +181,14 @@ class GkdAbService : CompositionAbService({
return@launchTry
}
if (!matchApp) continue
val target = rule.query(nodeVal) ?: continue
val target =
rule.query(nodeVal, if (common) defaultCacheTransform else null)
if (common) {
defaultCacheTransform.indexCache.clear()
}
if (target == null) {
continue
}
if (activityRule !== getAndUpdateCurrentRules()) break
if (rule.checkDelay() && rule.actionDelayJob == null) {
rule.actionDelayJob = scope.launch(queryThread) {
@ -521,8 +529,11 @@ class GkdAbService : CompositionAbService({
}
}
val targetNode =
serviceVal.safeActiveWindow?.querySelector(selector, gkdAction.quickFind)
?: throw RpcError("没有查询到节点")
serviceVal.safeActiveWindow?.querySelector(
selector,
gkdAction.quickFind,
if (selector.canCacheIndex) createCacheTransform().transform else null
) ?: throw RpcError("没有查询到节点")
if (gkdAction.action == null) {
// 仅查询

View File

@ -371,6 +371,7 @@ fun AppItemPage(
TextButton(onClick = {
if (oldSource == source) {
toast("规则无变动")
setEditGroupRaw(null)
return@TextButton
}
val newGroupRaw = try {
@ -446,6 +447,7 @@ fun AppItemPage(
TextButton(onClick = {
if (oldSource == source) {
toast("禁用项无变动")
setExcludeGroupRaw(null)
return@TextButton
}
setExcludeGroupRaw(null)

View File

@ -346,6 +346,7 @@ fun GlobalRuleExcludePage(subsItemId: Long, groupKey: Int) {
TextButton(onClick = {
if (oldSource == source) {
toast("禁用项无变动")
showEditDlg = false
return@TextButton
}
showEditDlg = false

View File

@ -343,6 +343,7 @@ fun GlobalRulePage(subsItemId: Long, focusGroupKey: Int? = null) {
confirmButton = {
TextButton(onClick = {
if (oldSource == source) {
setEditGroupRaw(null)
toast("规则无变动")
return@TextButton
}

View File

@ -1,7 +1,6 @@
package li.songe.gkd.ui.home
import android.app.Activity
import android.content.Intent
import android.webkit.URLUtil
import androidx.compose.material3.Icon
import androidx.compose.material3.NavigationBar
import androidx.compose.material3.NavigationBarItem
@ -18,6 +17,7 @@ import androidx.hilt.navigation.compose.hiltViewModel
import com.blankj.utilcode.util.LogUtils
import com.ramcosta.composedestinations.annotation.Destination
import com.ramcosta.composedestinations.annotation.RootNavGraph
import li.songe.gkd.MainActivity
import li.songe.gkd.util.ProfileTransitions
data class BottomNavItem(
@ -29,13 +29,19 @@ data class BottomNavItem(
@Destination(style = ProfileTransitions::class)
@Composable
fun HomePage() {
val context = LocalContext.current as MainActivity
val vm = hiltViewModel<HomeVm>()
val tab by vm.tabFlow.collectAsState()
val intent: Intent? = (LocalContext.current as Activity).intent
val intent = context.intent
LaunchedEffect(key1 = intent, block = {
if (intent != null) {
LogUtils.d(intent)
context.intent = null
val data = intent.data
val url = data?.getQueryParameter("url")
if (data?.scheme == "gkd" && data.host == "import" && URLUtil.isNetworkUrl(url)) {
LogUtils.d(data, url)
}
}
})

View File

@ -16,6 +16,11 @@ kotlin {
generateTypeScriptDefinitions()
browser {}
}
sourceSets {
all {
languageSettings.optIn("kotlin.js.ExperimentalJsExport")
}
}
sourceSets["commonMain"].dependencies {
implementation(libs.kotlin.stdlib.common)
}

View File

@ -1,30 +0,0 @@
package li.songe.selector
import kotlin.js.ExperimentalJsExport
import kotlin.js.JsExport
@OptIn(ExperimentalJsExport::class)
@JsExport
data class ExtSyntaxError internal constructor(
val expectedValue: String,
val position: Int,
val source: String,
) : Exception(
"expected $expectedValue in selector at position $position, but got ${
source.getOrNull(
position
)
}"
) {
internal companion object {
fun assert(source: String, offset: Int, value: String = "", expectedValue: String? = null) {
if (offset >= source.length || (value.isNotEmpty() && !value.contains(source[offset]))) {
throw ExtSyntaxError(expectedValue ?: value, offset, source)
}
}
fun throwError(source: String, offset: Int, expectedValue: String = ""): Nothing {
throw ExtSyntaxError(expectedValue, offset, source)
}
}
}

View File

@ -0,0 +1,31 @@
package li.songe.selector
import kotlin.js.JsExport
@JsExport
data class GkdSyntaxError internal constructor(
val expectedValue: String,
val position: Int,
val source: String,
) : Exception(
"expected $expectedValue in selector at position $position, but got ${
source.getOrNull(
position
)
}"
)
internal fun gkdAssert(
source: String,
offset: Int,
value: String = "",
expectedValue: String? = null
) {
if (offset >= source.length || (value.isNotEmpty() && !value.contains(source[offset]))) {
throw GkdSyntaxError(expectedValue ?: value, offset, source)
}
}
internal fun gkdError(source: String, offset: Int, expectedValue: String = ""): Nothing {
throw GkdSyntaxError(expectedValue, offset, source)
}

View File

@ -1,12 +1,10 @@
package li.songe.selector
import kotlin.js.ExperimentalJsExport
import kotlin.js.JsExport
@OptIn(ExperimentalJsExport::class)
@JsExport
class CommonSelector private constructor(
@Suppress("UNUSED")
class MultiplatformSelector private constructor(
internal val selector: Selector,
) {
val tracks = selector.tracks
@ -19,21 +17,24 @@ class CommonSelector private constructor(
val qfTextValue = selector.qfTextValue
val canQf = selector.canQf
val isMatchRoot = selector.isMatchRoot
fun checkType(getType: (String) -> String): Boolean {
return selector.checkType(getType)
}
fun <T : Any> match(node: T, transform: CommonTransform<T>): T? {
fun <T : Any> match(node: T, transform: MultiplatformTransform<T>): T? {
return selector.match(node, transform.transform)
}
@Suppress("UNCHECKED_CAST")
fun <T : Any> matchTrack(node: T, transform: CommonTransform<T>): Array<T>? {
fun <T : Any> matchTrack(node: T, transform: MultiplatformTransform<T>): Array<T>? {
return selector.matchTracks(node, transform.transform)?.toTypedArray<Any?>() as Array<T>?
}
override fun toString() = selector.toString()
companion object {
fun parse(source: String) = CommonSelector(Selector.parse(source))
fun parseOrNull(source: String) = Selector.parseOrNull(source)?.let(::CommonSelector)
fun parse(source: String) = MultiplatformSelector(Selector.parse(source))
fun parseOrNull(source: String) = Selector.parseOrNull(source)?.let(::MultiplatformSelector)
}
}

View File

@ -1,11 +1,10 @@
package li.songe.selector
import kotlin.js.ExperimentalJsExport
import kotlin.js.JsExport
@OptIn(ExperimentalJsExport::class)
@JsExport
class CommonTransform<T : Any>(
@Suppress("UNCHECKED_CAST", "UNUSED")
class MultiplatformTransform<T : Any>(
getAttr: (T, String) -> Any?,
getName: (T) -> String?,
getChildren: (T) -> Array<T>,
@ -18,28 +17,23 @@ class CommonTransform<T : Any>(
getParent = getParent,
)
@Suppress("UNCHECKED_CAST", "UNUSED")
val querySelectorAll: (T, CommonSelector) -> Array<T> = { node, selector ->
val querySelectorAll: (T, MultiplatformSelector) -> Array<T> = { node, selector ->
val result =
transform.querySelectorAll(node, selector.selector).toList().toTypedArray<Any?>()
result as Array<T>
}
@Suppress("UNUSED")
val querySelector: (T, CommonSelector) -> T? = { node, selector ->
val querySelector: (T, MultiplatformSelector) -> T? = { node, selector ->
transform.querySelectorAll(node, selector.selector).firstOrNull()
}
@Suppress("UNCHECKED_CAST", "UNUSED")
val querySelectorTrackAll: (T, CommonSelector) -> Array<Array<T>> = { node, selector ->
val querySelectorTrackAll: (T, MultiplatformSelector) -> Array<Array<T>> = { node, selector ->
val result = transform.querySelectorTrackAll(node, selector.selector)
.map { it.toTypedArray<Any?>() as Array<T> }.toList().toTypedArray<Any?>()
result as Array<Array<T>>
}
@Suppress("UNCHECKED_CAST", "UNUSED")
val querySelectorTrack: (T, CommonSelector) -> Array<T>? = { node, selector ->
val querySelectorTrack: (T, MultiplatformSelector) -> Array<T>? = { node, selector ->
transform.querySelectorTrackAll(node, selector.selector).firstOrNull()
?.toTypedArray<Any?>() as Array<T>?
}

View File

@ -1,17 +0,0 @@
package li.songe.selector
internal interface NodeMatchFc {
operator fun <T> invoke(node: T, transform: Transform<T>): Boolean
}
interface NodeSequenceFc {
operator fun <T> invoke(sq: Sequence<T?>): Sequence<T?>
}
internal val emptyNodeSequence = object : NodeSequenceFc {
override fun <T> invoke(sq: Sequence<T?>) = emptySequence<T?>()
}
internal interface NodeTraversalFc {
operator fun <T> invoke(node: T, transform: Transform<T>): Sequence<T?>
}

View File

@ -2,54 +2,32 @@ 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
class Selector internal constructor(private val propertyWrapper: PropertyWrapper) {
override fun toString(): String {
return propertyWrapper.toString()
}
val tracks by lazy {
val tracks = run {
val list = mutableListOf(propertyWrapper)
while (true) {
list.add(list.last().to?.to ?: break)
}
list.map { p -> p.propertySegment.tracked }.toTypedArray()
list.map { p -> p.propertySegment.tracked }.toTypedArray<Boolean>()
}
val trackIndex = tracks.indexOfFirst { it }.let { i ->
if (i < 0) 0 else i
}
val connectKeys by lazy {
var c = propertyWrapper.to
val keys = mutableListOf<String>()
while (c != null) {
c?.apply {
keys.add(connectSegment.operator.key)
}
c = c?.to?.to
}
keys.toTypedArray()
}
val propertyNames by lazy {
var p: PropertyWrapper? = propertyWrapper
val names = mutableSetOf<String>()
while (p != null) {
val s = p!!.propertySegment
p = p!!.to?.to
names.addAll(s.propertyNames)
}
names.distinct().toTypedArray()
}
fun <T> match(
node: T,
transform: Transform<T>,
trackNodes: MutableList<T> = mutableListOf(),
trackNodes: MutableList<T> = ArrayList(tracks.size),
): T? {
val trackTempNodes = matchTracks(node, transform, trackNodes) ?: return null
return trackTempNodes[trackIndex]
@ -58,30 +36,30 @@ class Selector internal constructor(private val propertyWrapper: PropertyWrapper
fun <T> matchTracks(
node: T,
transform: Transform<T>,
trackNodes: MutableList<T> = mutableListOf(),
trackNodes: MutableList<T> = ArrayList(tracks.size),
): List<T>? {
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 String) {
e.value
if (e is BinaryExpression && e.name == "id" && e.operator == CompareOperator.Equal && e.value is PrimitiveValue.StringValue) {
e.value.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 String) {
e.value
if (e is BinaryExpression && e.name == "vid" && e.operator == CompareOperator.Equal && e.value is PrimitiveValue.StringValue) {
e.value.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 String) {
e.value
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
} else {
null
}
@ -91,9 +69,58 @@ class Selector internal constructor(private val propertyWrapper: PropertyWrapper
// 主动查询
val isMatchRoot = propertyWrapper.propertySegment.expressions.firstOrNull().let { e ->
e is BinaryExpression && e.name == "depth" && e.operator == CompareOperator.Equal && e.value == 0
e is BinaryExpression && e.name == "depth" && e.operator == CompareOperator.Equal && e.value.value == 0
}
val connectKeys = run {
var c = propertyWrapper.to
val keys = mutableListOf<String>()
while (c != null) {
c.apply {
keys.add(connectSegment.operator.key)
}
c = c.to.to
}
keys.toTypedArray()
}
private val binaryExpressions = run {
var p: PropertyWrapper? = propertyWrapper
val names = mutableListOf<BinaryExpression>()
while (p != null) {
val s = p.propertySegment
names.addAll(s.binaryExpressions)
p = p.to?.to
}
names.distinct().toTypedArray()
}
val propertyNames = run {
binaryExpressions.map { e -> e.name }.distinct().toTypedArray()
}
fun checkType(getType: (String) -> String): Boolean {
binaryExpressions.forEach { e ->
if (e.value.value != null) {
val type = getType(e.name)
if (!(when (type) {
"boolean" -> e.value is PrimitiveValue.BooleanValue
"int" -> e.value is PrimitiveValue.IntValue
"string" -> e.value is PrimitiveValue.StringValue
else -> false
})
) return false
}
}
return false
}
val canCacheIndex =
connectKeys.contains(ConnectOperator.BeforeBrother.key) || connectKeys.contains(
ConnectOperator.AfterBrother.key
) || propertyNames.contains("index")
companion object {
fun parse(source: String) = ParserSet.selectorParser(source)
fun parseOrNull(source: String) = try {

View File

@ -1,54 +1,14 @@
package li.songe.selector
import li.songe.selector.data.ConnectExpression
@Suppress("UNUSED")
class Transform<T>(
val getAttr: (T, String) -> Any?,
val getName: (T) -> CharSequence?,
val getChildren: (T) -> Sequence<T>,
val getChild: (T, Int) -> T? = { node, offset -> getChildren(node).elementAtOrNull(offset) },
val getParent: (T) -> T?,
val getAncestors: (T) -> Sequence<T> = { node ->
sequence {
var parentVar: T? = getParent(node) ?: return@sequence
while (parentVar != null) {
parentVar?.let {
yield(it)
parentVar = getParent(it)
}
}
}
},
val getAncestor: (T, Int) -> T? = { node, offset -> getAncestors(node).elementAtOrNull(offset) },
val getBeforeBrothers: (T) -> Sequence<T?> = { node ->
sequence {
val parentVal = getParent(node) ?: return@sequence
val list = getChildren(parentVal).takeWhile { it != node }.toMutableList()
list.reverse()
yieldAll(list)
}
},
val getBeforeBrother: (T, Int) -> T? = { node, offset ->
getBeforeBrothers(node).elementAtOrNull(
offset
)
},
val getAfterBrothers: (T) -> Sequence<T?> = { node ->
sequence {
val parentVal = getParent(node) ?: return@sequence
yieldAll(getChildren(parentVal).dropWhile { it != node }.drop(1))
}
},
val getAfterBrother: (T, Int) -> T? = { node, offset ->
getAfterBrothers(node).elementAtOrNull(
offset
)
},
/**
* 遍历下面所有子孙节点,不包含自己
*/
val getDescendants: (T) -> Sequence<T> = { node ->
sequence { // 深度优先 先序遍历
// https://developer.mozilla.org/zh-CN/docs/Web/API/Document/querySelector
@ -73,12 +33,112 @@ class Transform<T>(
}
},
) {
val getChildrenX: (T, ConnectExpression) -> Sequence<T> = { node, connectExpression ->
getChildren(node)
.let {
if (connectExpression.maxOffset != null) {
it.take(connectExpression.maxOffset!! + 1)
} else {
it
}
}
.filterIndexed { i, _ ->
connectExpression.checkOffset(
i
)
}
},
val getAncestors: (T, ConnectExpression) -> Sequence<T> = { node, connectExpression ->
sequence {
var parentVar: T? = getParent(node) ?: return@sequence
var offset = 0
while (parentVar != null) {
parentVar?.let {
if (connectExpression.checkOffset(offset)) {
yield(it)
}
offset++
connectExpression.maxOffset?.let { maxOffset ->
if (offset > maxOffset) {
return@sequence
}
}
parentVar = getParent(it)
}
}
}
},
val getBeforeBrothers: (T, ConnectExpression) -> Sequence<T> = { node, connectExpression ->
val parentVal = getParent(node)
if (parentVal != null) {
val list = getChildren(parentVal).takeWhile { it != node }.toMutableList()
list.reverse()
list.asSequence().filterIndexed { i, _ ->
connectExpression.checkOffset(
i
)
}
} else {
emptySequence()
}
},
val getAfterBrothers: (T, ConnectExpression) -> Sequence<T> = { node, connectExpression ->
val parentVal = getParent(node)
if (parentVal != null) {
getChildren(parentVal).dropWhile { it != node }
.drop(1)
.let {
if (connectExpression.maxOffset != null) {
it.take(connectExpression.maxOffset!! + 1)
} else {
it
}
}.filterIndexed { i, _ ->
connectExpression.checkOffset(
i
)
}
} else {
emptySequence()
}
},
val getDescendantsX: (T, ConnectExpression) -> Sequence<T> = { node, connectExpression ->
sequence {
val stack = getChildren(node).toMutableList()
if (stack.isEmpty()) return@sequence
stack.reverse()
val tempNodes = mutableListOf<T>()
var offset = 0
do {
val top = stack.removeLast()
if (connectExpression.checkOffset(offset)) {
yield(top)
}
offset++
connectExpression.maxOffset?.let { maxOffset ->
if (offset > maxOffset) {
return@sequence
}
}
for (childNode in getChildren(top)) {
tempNodes.add(childNode)
}
if (tempNodes.isNotEmpty()) {
for (i in tempNodes.size - 1 downTo 0) {
stack.add(tempNodes[i])
}
tempNodes.clear()
}
} while (stack.isNotEmpty())
}
},
) {
val querySelectorAll: (T, Selector) -> Sequence<T> = { node, selector ->
sequence {
// cache trackNodes
val trackNodes: MutableList<T> = mutableListOf()
val trackNodes = ArrayList<T>(selector.tracks.size)
val r0 = selector.match(node, this@Transform, trackNodes)
if (r0 != null) yield(r0)
getDescendants(node).forEach { childNode ->
@ -104,7 +164,6 @@ class Transform<T>(
}
}
@Suppress("UNUSED")
val querySelectorTrack: (T, Selector) -> List<T>? = { node, selector ->
querySelectorTrackAll(
node, selector

View File

@ -2,50 +2,16 @@ package li.songe.selector.data
import li.songe.selector.Transform
data class BinaryExpression(val name: String, val operator: CompareOperator, val value: Any?) :
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)
operator.compare(transform.getAttr(node, name), value.value)
override val propertyNames = listOf(name)
override val binaryExpressions = listOf(this)
override fun toString() = "${name}${operator}${
if (value is String) {
val wrapChar = '"'
val sb = StringBuilder()
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 10..0x1f -> {
sb.append("\\x" + c.code.toString(16))
}
else -> {
sb.append(c)
}
}
}
}
sb.append(wrapChar)
sb.toString()
} else {
value
}
}"
override fun toString() = "${name}${operator.key}${value}"
}

View File

@ -1,8 +1,8 @@
package li.songe.selector.data
sealed class CompareOperator(val key: String) {
override fun toString() = key
abstract fun compare(left: Any?, right: Any?): Boolean
abstract fun allowType(type: PrimitiveValue): Boolean
companion object {
// https://stackoverflow.com/questions/47648689
@ -35,70 +35,96 @@ sealed class CompareOperator(val key: String) {
left == right
}
}
override fun allowType(type: PrimitiveValue) = true
}
data object NotEqual : CompareOperator("!=") {
override fun compare(left: Any?, right: Any?) = !Equal.compare(left, right)
override fun allowType(type: PrimitiveValue) = true
}
data object Start : CompareOperator("^=") {
override fun compare(left: Any?, right: Any?): Boolean {
return if (left is CharSequence && right is CharSequence) left.startsWith(right) else false
}
override fun allowType(type: PrimitiveValue) = type is PrimitiveValue.StringValue
}
data object NotStart : CompareOperator("!^=") {
override fun compare(left: Any?, right: Any?): Boolean {
return if (left is CharSequence && right is CharSequence) !left.startsWith(right) else false
}
override fun allowType(type: PrimitiveValue) = type is PrimitiveValue.StringValue
}
data object Include : CompareOperator("*=") {
override fun compare(left: Any?, right: Any?): Boolean {
return if (left is CharSequence && right is CharSequence) left.contains(right) else false
}
override fun allowType(type: PrimitiveValue) = type is PrimitiveValue.StringValue
}
data object NotInclude : CompareOperator("!*=") {
override fun compare(left: Any?, right: Any?): Boolean {
return if (left is CharSequence && right is CharSequence) !left.contains(right) else false
}
override fun allowType(type: PrimitiveValue) = type is PrimitiveValue.StringValue
}
data object End : CompareOperator("$=") {
override fun compare(left: Any?, right: Any?): Boolean {
return if (left is CharSequence && right is CharSequence) left.endsWith(right) else false
}
override fun allowType(type: PrimitiveValue) = type is PrimitiveValue.StringValue
}
data object NotEnd : CompareOperator("!$=") {
override fun compare(left: Any?, right: Any?): Boolean {
return if (left is CharSequence && right is CharSequence) !left.endsWith(right) else false
}
override fun allowType(type: PrimitiveValue) = type is PrimitiveValue.StringValue
}
data object Less : CompareOperator("<") {
override fun compare(left: Any?, right: Any?): Boolean {
return if (left is Int && right is Int) left < right else false
}
override fun allowType(type: PrimitiveValue) = type is PrimitiveValue.IntValue
}
data object LessEqual : CompareOperator("<=") {
override fun compare(left: Any?, right: Any?): Boolean {
return if (left is Int && right is Int) left <= right else false
}
override fun allowType(type: PrimitiveValue) = type is PrimitiveValue.IntValue
}
data object More : CompareOperator(">") {
override fun compare(left: Any?, right: Any?): Boolean {
return if (left is Int && right is Int) left > right else false
}
override fun allowType(type: PrimitiveValue) = type is PrimitiveValue.IntValue
}
data object MoreEqual : CompareOperator(">=") {
override fun compare(left: Any?, right: Any?): Boolean {
return if (left is Int && right is Int) left >= right else false
}
override fun allowType(type: PrimitiveValue) = type is PrimitiveValue.IntValue
}
}

View File

@ -1,10 +1,8 @@
package li.songe.selector.data
import li.songe.selector.NodeSequenceFc
sealed class ConnectExpression {
abstract val isConstant: Boolean
abstract val minOffset: Int
internal abstract val traversal: NodeSequenceFc
abstract val maxOffset: Int?
abstract fun checkOffset(offset: Int): Boolean
abstract fun getOffset(i: Int): Int
}

View File

@ -3,9 +3,9 @@ package li.songe.selector.data
import li.songe.selector.Transform
sealed class ConnectOperator(val key: String) {
override fun toString() = key
abstract fun <T> traversal(node: T, transform: Transform<T>): Sequence<T?>
abstract fun <T> traversal(node: T, transform: Transform<T>, offset: Int): T?
abstract fun <T> traversal(
node: T, transform: Transform<T>, connectExpression: ConnectExpression
): Sequence<T>
companion object {
// https://stackoverflow.com/questions/47648689
@ -20,54 +20,47 @@ sealed class ConnectOperator(val key: String) {
* A + B, 1,2,3,A,B,7,8
*/
data object BeforeBrother : ConnectOperator("+") {
override fun <T> traversal(node: T, transform: Transform<T>) =
transform.getBeforeBrothers(node)
override fun <T> traversal(
node: T, transform: Transform<T>, connectExpression: ConnectExpression
) = transform.getBeforeBrothers(node, connectExpression)
override fun <T> traversal(node: T, transform: Transform<T>, offset: Int): T? =
transform.getBeforeBrother(node, offset)
}
/**
* A - B, 1,2,3,B,A,7,8
*/
data object AfterBrother : ConnectOperator("-") {
override fun <T> traversal(node: T, transform: Transform<T>) =
transform.getAfterBrothers(node)
override fun <T> traversal(node: T, transform: Transform<T>, offset: Int): T? =
transform.getAfterBrother(node, offset)
override fun <T> traversal(
node: T, transform: Transform<T>, connectExpression: ConnectExpression
) = transform.getAfterBrothers(node, connectExpression)
}
/**
* A > B, A is the ancestor of B
*/
data object Ancestor : ConnectOperator(">") {
override fun <T> traversal(node: T, transform: Transform<T>) = transform.getAncestors(node)
override fun <T> traversal(
node: T, transform: Transform<T>, connectExpression: ConnectExpression
) = transform.getAncestors(node, connectExpression)
override fun <T> traversal(node: T, transform: Transform<T>, offset: Int): T? =
transform.getAncestor(node, offset)
}
/**
* A < B, A is the child of B
*/
data object Child : ConnectOperator("<") {
override fun <T> traversal(node: T, transform: Transform<T>) = transform.getChildren(node)
override fun <T> traversal(node: T, transform: Transform<T>, offset: Int): T? =
transform.getChild(node, offset)
override fun <T> traversal(
node: T, transform: Transform<T>, connectExpression: ConnectExpression
) = transform.getChildrenX(node, connectExpression)
}
/**
* A << B, A is the descendant of B
*/
data object Descendant : ConnectOperator("<<") {
override fun <T> traversal(node: T, transform: Transform<T>) =
transform.getDescendants(node)
override fun <T> traversal(node: T, transform: Transform<T>, offset: Int): T? =
transform.getDescendants(node).elementAtOrNull(offset)
override fun <T> traversal(
node: T, transform: Transform<T>, connectExpression: ConnectExpression
) = transform.getDescendantsX(node, connectExpression)
}
}

View File

@ -1,7 +1,6 @@
package li.songe.selector.data
import li.songe.selector.Transform
import li.songe.selector.NodeTraversalFc
data class ConnectSegment(
val operator: ConnectOperator = ConnectOperator.Ancestor,
@ -11,26 +10,10 @@ data class ConnectSegment(
if (operator == ConnectOperator.Ancestor && connectExpression is PolynomialExpression && connectExpression.a == 1 && connectExpression.b == 0) {
return ""
}
return operator.toString() + connectExpression.toString()
return operator.key + connectExpression.toString()
}
internal val traversal = if (connectExpression.isConstant) {
object : NodeTraversalFc {
override fun <T> invoke(node: T, transform: Transform<T>): Sequence<T?> = sequence {
val node1 = operator.traversal(node, transform, connectExpression.minOffset)
if (node1 != null) {
yield(node1)
}
}
}
} else {
object : NodeTraversalFc {
override fun <T> invoke(node: T, transform: Transform<T>): Sequence<T?> {
return connectExpression.traversal(
operator.traversal(node, transform)
)
}
}
fun <T> traversal(node: T, transform: Transform<T>): Sequence<T?> {
return operator.traversal(node, transform, connectExpression)
}
}

View File

@ -12,7 +12,7 @@ data class ConnectWrapper(
fun <T> matchTracks(
node: T, transform: Transform<T>,
trackNodes: MutableList<T> = mutableListOf(),
trackNodes: MutableList<T>,
): List<T>? {
connectSegment.traversal(node, transform).forEach {
if (it == null) return@forEach

View File

@ -5,5 +5,5 @@ import li.songe.selector.Transform
sealed class Expression {
abstract fun <T> match(node: T, transform: Transform<T>): Boolean
abstract val propertyNames: List<String>
abstract val binaryExpressions: List<BinaryExpression>
}

View File

@ -11,7 +11,7 @@ data class LogicalExpression(
return operator.compare(node, transform, left, right)
}
override val propertyNames = left.propertyNames + right.propertyNames
override val binaryExpressions = left.binaryExpressions + right.binaryExpressions
override fun toString(): String {
val leftStr = if (left is LogicalExpression && left.operator != operator) {
@ -24,6 +24,6 @@ data class LogicalExpression(
} else {
right.toString()
}
return "$leftStr\u0020$operator\u0020$rightStr"
return "$leftStr\u0020${operator.key}\u0020$rightStr"
}
}

View File

@ -12,7 +12,6 @@ sealed class LogicalOperator(val key: String) {
}
}
override fun toString() = key
abstract fun <T> compare(
node: T,
transform: Transform<T>,

View File

@ -1,7 +1,5 @@
package li.songe.selector.data
import li.songe.selector.NodeSequenceFc
/**
* an+b
*/
@ -30,50 +28,112 @@ data class PolynomialExpression(val a: Int = 0, val b: Int = 1) : ConnectExpress
return "(${a}n${bOp}${b})"
}
val numbers = if (a < 0) {
if (b < 0) {
emptyList()
} else if (b > 0) {
private fun invalidValue(): Nothing {
error("invalid PolynomialExpression: a=$a, b=$b")
}
override val minOffset = if (a > 0) {
if (b > 0) {
a + b
} else if (b == 0) {
a
} else {
// 2n-10 -> n>=6
// 3n-10 -> n>=4
// 3n-3 -> n>=2
// 3n-1 -> n>=1
// an+b>0 -> n>-b/a
val minN = -b / a + 1
a * minN + b
}
} else if (a == 0) {
if (b > 0) {
b
} else {
invalidValue()
}
} else {
if (b > 0) {
if (b <= -a) {
emptyList()
invalidValue()
} else {
val list = mutableListOf<Int>()
var n = 1
while (a * n + b > 0) {
list.add(a * n + b)
n++
}
list.sorted()
// -2n+9 -> (1_7,2_5,3_3,4_1) -> (1,3,5,7) -> 1
// -3n+9 -> (1_6,2_3) -> (3,6)
// -5n+7 -> (1_2) -> (2)
val maxN = -b / a - if (b % a == 0) 1 else 0
a * maxN + b
}
} else {
emptyList()
invalidValue()
}
} else if (a > 0) {
// infinite
emptyList()
} else {
if (b < 0) {
emptyList()
} else if (b > 0) {
listOf(b)
} - 1
override val maxOffset = if (a > 0) {
null
} else if (a == 0) {
if (b > 0) {
b
} else {
emptyList()
invalidValue()
}
}
override val isConstant = numbers.size == 1
override val minOffset = (numbers.firstOrNull() ?: 1) - 1
private val b1 = b - 1
private val maxAb = a + b // when a<=0
override val traversal = object : NodeSequenceFc {
override fun <T> invoke(sq: Sequence<T?>): Sequence<T?> {
return (if (a > 0) {
sq
} else {
if (b > 0) {
if (b <= -a) {
invalidValue()
} else {
sq.take(maxAb)
}).filterIndexed { x, _ -> (x - b1) % a == 0 && (x - b1) / a > 0 }
a + b
}
} else {
invalidValue()
}
} - 1
private val isConstant = minOffset == maxOffset
// (2n-1) -> (1,3,5) -> [0,2,4]
override fun checkOffset(offset: Int): Boolean {
if (isConstant) {
return offset == minOffset
}
val y = (offset + 1) - b
return y % a == 0 && y / a >= 1
}
private val innerGetOffset: (Int) -> Int = if (a > 0) {
if (b > 0) {
{ i -> a * i + b }
} else if (b == 0) {
{ i -> a * i + b }
} else {
val minN = -b / a + 1
{ i -> a * (minN + i) + b }
}
} else if (a == 0) {
if (b > 0) {
{ i ->
if (i != 0) {
invalidValue()
}
b
}
} else {
invalidValue()
}
} else {
if (b > 0) {
if (b <= -a) {
invalidValue()
} else {
// -2n+9 -> (1_7,2_5,3_3,4_1) -> (1,3,5,7) -> 1
// -3n+9 -> (1_6,2_3) -> (3,6)
// -5n+7 -> (1_2) -> (2)
val maxN = -b / a - if (b % a == 0) 1 else 0
{ i -> a * (maxN - i) + b }
}
} else {
invalidValue()
}
}
override fun getOffset(i: Int) = innerGetOffset(i) - 1
}

View File

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

View File

@ -1,6 +1,5 @@
package li.songe.selector.data
import li.songe.selector.NodeMatchFc
import li.songe.selector.Transform
@ -14,31 +13,22 @@ data class PropertySegment(
) {
private val matchAnyName = name.isBlank() || name == "*"
val propertyNames =
(if (matchAnyName) listOf("name") else emptyList()) + expressions.map { e -> e.propertyNames }
.flatten()
val binaryExpressions = expressions.map { e -> e.binaryExpressions }.flatten()
override fun toString(): String {
val matchTag = if (tracked) "@" else ""
return matchTag + name + expressions.joinToString("") { "[$it]" }
}
private val matchName = if (matchAnyName) {
object : NodeMatchFc {
override fun <T> invoke(node: T, transform: Transform<T>) = true
}
} else {
object : NodeMatchFc {
override fun <T> invoke(node: T, transform: Transform<T>): Boolean {
val str = transform.getName(node) ?: return false
if (str.length == name.length) {
return str.contentEquals(name)
} else if (str.length > name.length) {
return str[str.length - name.length - 1] == '.' && str.endsWith(name)
}
return false
}
private fun <T> matchName(node: T, transform: Transform<T>): Boolean {
if (matchAnyName) return true
val str = transform.getName(node) ?: return false
if (str.length == name.length) {
return str.contentEquals(name)
} else if (str.length > name.length) {
return str[str.length - name.length - 1] == '.' && str.endsWith(name)
}
return false
}
fun <T> match(node: T, transform: Transform<T>): Boolean {

View File

@ -17,7 +17,7 @@ data class PropertyWrapper(
fun <T> matchTracks(
node: T,
transform: Transform<T>,
trackNodes: MutableList<T> = mutableListOf(),
trackNodes: MutableList<T>,
): List<T>? {
if (!propertySegment.match(node, transform)) {
return null

View File

@ -1,18 +1,18 @@
package li.songe.selector.data
import li.songe.selector.NodeSequenceFc
import li.songe.selector.util.filterIndexes
data class TupleExpression(
val numbers: List<Int>,
) : ConnectExpression() {
override val isConstant = numbers.size == 1
override val minOffset = (numbers.firstOrNull() ?: 1) - 1
override val maxOffset = numbers.lastOrNull()
private val indexes = numbers.map { x -> x - 1 }
override val traversal: NodeSequenceFc = object : NodeSequenceFc {
override fun <T> invoke(sq: Sequence<T?>): Sequence<T?> {
return sq.filterIndexes(indexes)
}
override fun checkOffset(offset: Int): Boolean {
return indexes.binarySearch(offset) >= 0
}
override fun getOffset(i: Int): Int {
return numbers[i]
}
override fun toString(): String {

View File

@ -1,6 +1,5 @@
package li.songe.selector.parser
import li.songe.selector.ExtSyntaxError
import li.songe.selector.Selector
import li.songe.selector.data.BinaryExpression
import li.songe.selector.data.CompareOperator
@ -12,9 +11,12 @@ import li.songe.selector.data.Expression
import li.songe.selector.data.LogicalExpression
import li.songe.selector.data.LogicalOperator
import li.songe.selector.data.PolynomialExpression
import li.songe.selector.data.PrimitiveValue
import li.songe.selector.data.PropertySegment
import li.songe.selector.data.PropertyWrapper
import li.songe.selector.data.TupleExpression
import li.songe.selector.gkdAssert
import li.songe.selector.gkdError
internal object ParserSet {
val whiteCharParser = Parser("\u0020\t\r\n") { source, offset, prefix ->
@ -27,7 +29,7 @@ internal object ParserSet {
ParserResult(data, i - offset)
}
val whiteCharStrictParser = Parser("\u0020\t\r\n") { source, offset, prefix ->
ExtSyntaxError.assert(source, offset, prefix, "whitespace")
gkdAssert(source, offset, prefix, "whitespace")
whiteCharParser(source, offset)
}
val nameParser =
@ -37,7 +39,7 @@ internal object ParserSet {
if ((s0 != null) && !prefix.contains(s0)) {
return@Parser ParserResult("")
}
ExtSyntaxError.assert(source, i, prefix, "*0-9a-zA-Z_")
gkdAssert(source, i, prefix, "*0-9a-zA-Z_")
var data = source[i].toString()
i++
if (data == "*") { // 范匹配
@ -47,7 +49,7 @@ internal object ParserSet {
while (i < source.length) {
// . 不能在开头和结尾
if (data[i - offset - 1] == '.') {
ExtSyntaxError.assert(source, i, prefix, "[0-9a-zA-Z_]")
gkdAssert(source, i, prefix, "[0-9a-zA-Z_]")
}
if (center.contains(source[i])) {
data += source[i]
@ -65,13 +67,13 @@ internal object ParserSet {
source.startsWith(
subOperator.key, offset
)
} ?: ExtSyntaxError.throwError(source, offset, "ConnectOperator")
} ?: gkdError(source, offset, "ConnectOperator")
ParserResult(operator, operator.key.length)
}
val integerParser = Parser("1234567890") { source, offset, prefix ->
var i = offset
ExtSyntaxError.assert(source, i, prefix, "number")
gkdAssert(source, i, prefix, "number")
var s = ""
while (i < source.length && prefix.contains(source[i])) {
s += source[i]
@ -81,7 +83,7 @@ internal object ParserSet {
try {
s.toInt()
} catch (e: NumberFormatException) {
ExtSyntaxError.throwError(source, offset, "valid format number")
gkdError(source, offset, "valid format number")
}, i - offset
)
}
@ -90,7 +92,7 @@ internal object ParserSet {
// [+-][a][n]
val monomialParser = Parser("+-1234567890n") { source, offset, prefix ->
var i = offset
ExtSyntaxError.assert(source, i, prefix)
gkdAssert(source, i, prefix)
/**
* one of 1, -1
*/
@ -109,7 +111,7 @@ internal object ParserSet {
}
i += whiteCharParser(source, i).length
// [a][n]
ExtSyntaxError.assert(source, i, integerParser.prefix + "n")
gkdAssert(source, i, integerParser.prefix + "n")
val coefficient = if (integerParser.prefix.contains(source[i])) {
val coefficientResult = integerParser(source, i)
i += coefficientResult.length
@ -132,23 +134,23 @@ internal object ParserSet {
// (+-an+-b)
val polynomialExpressionParser = Parser("(0123456789n") { source, offset, prefix ->
var i = offset
ExtSyntaxError.assert(source, i, prefix)
gkdAssert(source, i, prefix)
val monomialResultList = mutableListOf<ParserResult<Pair<Int, Int>>>()
when (source[i]) {
'(' -> {
i++
i += whiteCharParser(source, i).length
ExtSyntaxError.assert(source, i, monomialParser.prefix)
gkdAssert(source, i, monomialParser.prefix)
while (source[i] != ')') {
if (monomialResultList.size > 0) {
ExtSyntaxError.assert(source, i, "+-")
gkdAssert(source, i, "+-")
}
val monomialResult = monomialParser(source, i)
monomialResultList.add(monomialResult)
i += monomialResult.length
i += whiteCharParser(source, i).length
if (i >= source.length) {
ExtSyntaxError.assert(source, i, ")")
gkdAssert(source, i, ")")
}
}
i++
@ -167,21 +169,20 @@ internal object ParserSet {
}
map.mapKeys { power ->
if (power.key > 1) {
ExtSyntaxError.throwError(source, offset, "power must be 0 or 1")
gkdError(source, offset, "power must be 0 or 1")
}
}
val polynomialExpression = PolynomialExpression(map[1] ?: 0, map[0] ?: 0)
polynomialExpression.apply {
if ((a <= 0 && numbers.isEmpty()) || (numbers.isNotEmpty() && numbers.first() <= 0)) {
ExtSyntaxError.throwError(source, offset, "valid polynomialExpression")
}
val polynomialExpression = try {
PolynomialExpression(map[1] ?: 0, map[0] ?: 0)
} catch (e: Exception) {
gkdError(source, offset, "valid polynomialExpression")
}
ParserResult(polynomialExpression, i - offset)
}
val tupleExpressionParser = Parser { source, offset, _ ->
var i = offset
ExtSyntaxError.assert(source, i, "(")
gkdAssert(source, i, "(")
i++
val numbers = mutableListOf<Int>()
while (i < source.length && source[i] != ')') {
@ -189,11 +190,11 @@ internal object ParserSet {
val intResult = integerParser(source, i)
if (numbers.isEmpty()) {
if (intResult.data <= 0) {
ExtSyntaxError.throwError(source, i, "positive integer")
gkdError(source, i, "positive integer")
}
} else {
if (intResult.data <= numbers.last()) {
ExtSyntaxError.throwError(source, i, ">" + numbers.last())
gkdError(source, i, ">" + numbers.last())
}
}
i += intResult.length
@ -203,10 +204,10 @@ internal object ParserSet {
i++
i += whiteCharParser(source, i).length
// (1,2,3,) or (1, 2, 6)
ExtSyntaxError.assert(source, i, integerParser.prefix + ")")
gkdAssert(source, i, integerParser.prefix + ")")
}
}
ExtSyntaxError.assert(source, i, ")")
gkdAssert(source, i, ")")
i++
ParserResult(TupleExpression(numbers), i - offset)
}
@ -246,30 +247,30 @@ internal object ParserSet {
Parser(CompareOperator.allSubClasses.joinToString("") { it.key }) { source, offset, _ ->
val operator = CompareOperator.allSubClasses.find { compareOperator ->
source.startsWith(compareOperator.key, offset)
} ?: ExtSyntaxError.throwError(source, offset, "CompareOperator")
} ?: gkdError(source, offset, "CompareOperator")
ParserResult(operator, operator.key.length)
}
val stringParser = Parser("`'\"") { source, offset, prefix ->
var i = offset
ExtSyntaxError.assert(source, i, prefix)
gkdAssert(source, i, prefix)
val startChar = source[i]
i++
if (i >= source.length) {
ExtSyntaxError.throwError(source, i, "any char")
gkdError(source, i, "any char")
}
var data = ""
while (source[i] != startChar) {
if (i >= source.length - 1) {
ExtSyntaxError.assert(source, i, startChar.toString())
gkdAssert(source, i, startChar.toString())
break
}
// https://www.rfc-editor.org/rfc/inline-errata/rfc7159.html
if (source[i].code in 0x0000..0x001F) {
ExtSyntaxError.throwError(source, i, "0-1f escape char")
gkdError(source, i, "0-1f escape char")
}
if (source[i] == '\\') {
i++
ExtSyntaxError.assert(source, i)
gkdAssert(source, i)
data += when (source[i]) {
'\\' -> '\\'
'\'' -> '\''
@ -282,7 +283,7 @@ internal object ParserSet {
'x' -> {
repeat(2) {
i++
ExtSyntaxError.assert(source, i, "0123456789abcdefABCDEF")
gkdAssert(source, i, "0123456789abcdefABCDEF")
}
source.substring(i - 2 + 1, i + 1).toInt(16).toChar()
}
@ -290,13 +291,13 @@ internal object ParserSet {
'u' -> {
repeat(4) {
i++
ExtSyntaxError.assert(source, i, "0123456789abcdefABCDEF")
gkdAssert(source, i, "0123456789abcdefABCDEF")
}
source.substring(i - 4 + 1, i + 1).toInt(16).toChar()
}
else -> {
ExtSyntaxError.throwError(source, i, "escape char")
gkdError(source, i, "escape char")
}
}
} else {
@ -312,12 +313,12 @@ internal object ParserSet {
private val varStr = varPrefix + '.' + ('0'..'9').joinToString("")
val propertyParser = Parser(varPrefix) { source, offset, prefix ->
var i = offset
ExtSyntaxError.assert(source, i, prefix)
gkdAssert(source, i, prefix)
var data = source[i].toString()
i++
while (i < source.length && varStr.contains(source[i])) {
if (source[i] == '.') {
ExtSyntaxError.assert(source, i + 1, prefix)
gkdAssert(source, i + 1, prefix)
}
data += source[i]
i++
@ -326,51 +327,59 @@ internal object ParserSet {
}
val valueParser =
Parser("tfn" + stringParser.prefix + integerParser.prefix) { source, offset, prefix ->
Parser("tfn-" + stringParser.prefix + integerParser.prefix) { source, offset, prefix ->
var i = offset
ExtSyntaxError.assert(source, i, prefix)
val value: Any? = when (source[i]) {
gkdAssert(source, i, prefix)
val value: PrimitiveValue = when (source[i]) {
't' -> {
i++
"rue".forEach { c ->
ExtSyntaxError.assert(source, i, c.toString())
gkdAssert(source, i, c.toString())
i++
}
true
PrimitiveValue.BooleanValue(true)
}
'f' -> {
i++
"alse".forEach { c ->
ExtSyntaxError.assert(source, i, c.toString())
gkdAssert(source, i, c.toString())
i++
}
false
PrimitiveValue.BooleanValue(false)
}
'n' -> {
i++
"ull".forEach { c ->
ExtSyntaxError.assert(source, i, c.toString())
gkdAssert(source, i, c.toString())
i++
}
null
PrimitiveValue.NullValue
}
in stringParser.prefix -> {
val s = stringParser(source, i)
i += s.length
s.data
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
n.data
PrimitiveValue.IntValue(n.data)
}
else -> {
ExtSyntaxError.throwError(source, i, prefix)
gkdError(source, i, prefix)
}
}
ParserResult(value, i - offset)
@ -385,6 +394,9 @@ internal object ParserSet {
i += operatorResult.length
i += whiteCharParser(source, i).length
val valueResult = valueParser(source, i)
if (!operatorResult.data.allowType(valueResult.data)) {
gkdError(source, i, "valid primitive value")
}
i += valueResult.length
ParserResult(
BinaryExpression(
@ -398,7 +410,7 @@ internal object ParserSet {
i += whiteCharParser(source, i).length
val operator = LogicalOperator.allSubClasses.find { logicalOperator ->
source.startsWith(logicalOperator.key, offset)
} ?: ExtSyntaxError.throwError(source, offset, "LogicalOperator")
} ?: gkdError(source, offset, "LogicalOperator")
ParserResult(operator, operator.key.length)
}
@ -420,21 +432,21 @@ internal object ParserSet {
while (i - 1 >= count && source[i - 1 - count] in whiteCharParser.prefix) {
count++
}
ExtSyntaxError.throwError(
gkdError(
source, i - count - lastToken.length, "LogicalOperator"
)
}
}
i++
parserResults.add(expressionParser(source, i).apply { i += length })
ExtSyntaxError.assert(source, i, ")")
gkdAssert(source, i, ")")
i++
}
in "|&" -> {
parserResults.add(logicalOperatorParser(source, i).apply { i += length })
i += whiteCharParser(source, i).length
ExtSyntaxError.assert(source, i, "(" + propertyParser.prefix)
gkdAssert(source, i, "(" + propertyParser.prefix)
}
else -> {
@ -444,7 +456,7 @@ internal object ParserSet {
i += whiteCharParser(source, i).length
}
if (parserResults.isEmpty()) {
ExtSyntaxError.throwError(
gkdError(
source, i - offset, "Expression"
)
}
@ -486,12 +498,12 @@ internal object ParserSet {
val attrParser = Parser("[") { source, offset, prefix ->
var i = offset
ExtSyntaxError.assert(source, i, prefix)
gkdAssert(source, i, prefix)
i++
i += whiteCharParser(source, i).length
val exp = expressionParser(source, i)
i += exp.length
ExtSyntaxError.assert(source, i, "]")
gkdAssert(source, i, "]")
i++
ParserResult(
exp.data, i - offset
@ -515,7 +527,7 @@ internal object ParserSet {
expressions.add(attrResult.data)
}
if (nameResult.length == 0 && expressions.size == 0) {
ExtSyntaxError.throwError(source, i, "[")
gkdError(source, i, "[")
}
ParserResult(PropertySegment(tracked, nameResult.data, expressions), i - offset)
}
@ -537,6 +549,7 @@ internal object ParserSet {
i += whiteCharStrictParser(source, i).length
combinatorResult.data
} else {
// A B
ConnectSegment(connectExpression = PolynomialExpression(1, 0))
}
val selectorResult = selectorUnitParser(source, i)
@ -548,7 +561,7 @@ internal object ParserSet {
val endParser = Parser { source, offset, _ ->
if (offset != source.length) {
ExtSyntaxError.throwError(source, offset, "EOF")
gkdError(source, offset, "EOF")
}
ParserResult(Unit, 0)
}

View File

@ -1,42 +0,0 @@
package li.songe.selector.util
internal class FilterIndexesSequence<T>(
private val sequence: Sequence<T>,
private val indexes: List<Int>,
) : Sequence<T> {
override fun iterator() = object : Iterator<T> {
val iterator = sequence.iterator()
var seqIndex = 0 // sequence
var i = 0 // indexes
var nextItem: T? = null
fun calcNext(): T? {
if (seqIndex > indexes.last()) return null
while (iterator.hasNext()) {
val item = iterator.next()
if (indexes[i] == seqIndex) {
i++
seqIndex++
return item
}
seqIndex++
}
return null
}
override fun next(): T {
val result = nextItem
nextItem = null
return result ?: calcNext() ?: throw NoSuchElementException()
}
override fun hasNext(): Boolean {
nextItem = nextItem ?: calcNext()
return nextItem != null
}
}
}
internal fun <T> Sequence<T>.filterIndexes(indexes: List<Int>): Sequence<T> {
return FilterIndexesSequence(this, indexes)
}

View File

@ -6,12 +6,75 @@ import kotlinx.serialization.json.JsonNull
import kotlinx.serialization.json.booleanOrNull
import kotlinx.serialization.json.intOrNull
import li.songe.selector.parser.ParserSet
import li.songe.selector.util.filterIndexes
import org.junit.Test
import java.io.BufferedOutputStream
import java.io.File
import java.io.FileOutputStream
import java.net.URL
import java.util.zip.ZipInputStream
class ParserTest {
private val projectCwd = File("../").absolutePath
private val assetsDir = File("$projectCwd/_assets").apply {
if (!exists()) {
mkdir()
}
}
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 val idToSnapshot = HashMap<String, TestNode>()
private fun getOrDownloadNode(url: String): TestNode {
val githubAssetId = url.split('/').last()
idToSnapshot[githubAssetId]?.let { return it }
val file = assetsDir.resolve("$githubAssetId.json")
if (!file.exists()) {
URL("https://github.com/gkd-kit/inspect/files/${githubAssetId}/file.zip").openStream()
.use { inputStream ->
val zipInputStream = ZipInputStream(inputStream)
var entry = zipInputStream.nextEntry
while (entry != null) {
if (entry.name.endsWith(".json")) {
val outputStream = BufferedOutputStream(FileOutputStream(file))
val buffer = ByteArray(1024)
var bytesRead: Int
while (zipInputStream.read(buffer).also { bytesRead = it } != -1) {
outputStream.write(buffer, 0, bytesRead)
}
outputStream.close()
break
}
entry = zipInputStream.nextEntry
}
zipInputStream.closeEntry()
zipInputStream.close()
}
}
val nodes = json.decodeFromString<TestSnapshot>(file.readText()).nodes
nodes.forEach { node ->
node.parent = nodes.getOrNull(node.pid)
node.parent?.apply {
children.add(node)
}
}
return nodes.first().apply {
idToSnapshot[githubAssetId] = this
}
}
@Test
fun test_expression() {
@ -24,42 +87,26 @@ class ParserTest {
fun string_selector() {
val text =
"ImageView < @FrameLayout < LinearLayout < RelativeLayout <n LinearLayout < RelativeLayout + LinearLayout > RelativeLayout > TextView[text\$='广告']"
println("trackIndex: " + Selector.parse(text).trackIndex)
val selector = Selector.parse(text)
println("trackIndex: " + selector.trackIndex)
println("canCacheIndex: " + Selector.parse("A + B").canCacheIndex)
println("canCacheIndex: " + Selector.parse("A > B - C").canCacheIndex)
}
@Test
fun query_selector() {
val projectCwd = File("../").absolutePath
val text =
"* > View[isClickable=true][childCount=1][textLen=0] > Image[isClickable=false][textLen=0]"
"@[vid=\"rv_home_tab\"] <<(99-n) [vid=\"header_container\"] -(-2n+9) [vid=\"layout_refresh\"] +2 [vid=\"home_v10_frag_content\"]"
val selector = Selector.parse(text)
println("selector: $selector")
val jsonString = File("$projectCwd/_assets/snapshot-1686629593092.json").readText()
val json = Json {
ignoreUnknownKeys = true
}
val nodes = json.decodeFromString<TestSnapshot>(jsonString).nodes
nodes.forEach { node ->
node.parent = nodes.getOrNull(node.pid)
node.parent?.apply {
children.add(node)
}
}
val transform = Transform<TestNode>(getAttr = { node, name ->
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 })
val targets = transform.querySelectorAll(nodes.first(), selector).toList()
val node = getOrDownloadNode("https://i.gkd.li/i/14325747")
val targets = transform.querySelectorAll(node, selector).toList()
println("target_size: " + targets.size)
println("target_id: " + targets.map { t -> t.id })
assertTrue(targets.size == 1)
println("id: " + targets.first().id)
val trackTargets = transform.querySelectorTrackAll(nodes.first(), selector).toList()
val trackTargets = transform.querySelectorTrackAll(node, selector).toList()
println("trackTargets_size: " + trackTargets.size)
assertTrue(trackTargets.size == 1)
println(trackTargets.first().mapIndexed { index, testNode ->
@ -69,45 +116,22 @@ class ParserTest {
@Test
fun check_parser() {
println(Selector.parse("View > Text"))
val selector = Selector.parse("View > Text[index>-0]")
println("selector: $selector")
println("canCacheIndex: " + selector.canCacheIndex)
}
private val json = Json {
ignoreUnknownKeys = true
}
private fun getTreeNode(name: String): TestNode {
val jsonString = File("../_assets/$name").readText()
val nodes = json.decodeFromString<TestSnapshot>(jsonString).nodes
nodes.forEach { node ->
node.parent = nodes.getOrNull(node.pid)
node.parent?.apply {
children.add(node)
}
}
return nodes.first()
}
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 })
@Test
fun check_query() {
val text = "@TextView[text^='跳过'] + LinearLayout TextView[text*=`跳转`]"
val text = "@TextView - [text=\"签到提醒\"] <<n [vid=\"webViewContainer\"]"
val selector = Selector.parse(text)
println("selector: $selector")
println(selector.trackIndex)
println(selector.tracks.toList())
val snapshotNode = getTreeNode("snapshot-1693227637861.json")
val targets = transform.querySelectorAll(snapshotNode, selector).toList()
val node = getOrDownloadNode("https://i.gkd.li/i/14384152")
val targets = transform.querySelectorAll(node, selector).toList()
println("target_size: " + targets.size)
println(targets.firstOrNull())
}
@ -128,14 +152,6 @@ class ParserTest {
println("check_quote:$selector")
}
@Test
fun check_seq() {
println(
listOf(1, 2, 3, 4, 5, 6, 7, 8).asSequence().filterIndexes(listOf(0, 1, 7, 10)).toList()
)
println(listOf(0).asSequence().filterIndexes(listOf(0, 1, 7, 10)).toList())
}
@Test
fun check_tuple() {
val source = "[_id=15] >(1,2,9) X + Z >(7+9n) *"
@ -143,10 +159,9 @@ class ParserTest {
val selector = Selector.parse(source)
println("check_quote:$selector")
// https://i.gkd.li/import/13247733
// 1->3, 3->21
// 1,3->24
val snapshotNode = getTreeNode("snapshot-1698997584508.json")
val snapshotNode = getOrDownloadNode("https://i.gkd.li/i/13247733")
val (x1, x2) = (1..6).toList().shuffled().subList(0, 2).sorted()
val x1N =
transform.querySelectorAll(snapshotNode, Selector.parse("[_id=15] >$x1 *")).count()
@ -166,8 +181,7 @@ class ParserTest {
println("source:$source")
val selector = Selector.parse(source)
println("selector:$selector")
// https://i.gkd.li/import/13247610
val snapshotNode = getTreeNode("snapshot-1698990932472.json")
val snapshotNode = getOrDownloadNode("https://i.gkd.li/i/13247610")
println("result:" + transform.querySelectorAll(snapshotNode, selector).map { n -> n.id }
.toList())
}

View File

@ -15,5 +15,9 @@ data class TestNode(
@Transient
var children: MutableList<TestNode> = mutableListOf()
override fun toString(): String {
return id.toString()
}
}