Compare commits
No commits in common. "b6c6ef021aeed4ec0268aa0c4a2078bd133e6255" and "d0d79d2f07b29a54ae6629ea660c372dc447119b" have entirely different histories.
b6c6ef021a
...
d0d79d2f07
@ -120,18 +120,17 @@ which is used in `toString`) and hex encoding:
|
||||
|
||||
## Members
|
||||
|
||||
| name | meaning | type |
|
||||
|----------------------------|------------------------------------------------|---------------|
|
||||
| `size` | size | Int |
|
||||
| `decodeUtf8` | decode to String using UTF8 rules | Any |
|
||||
| `+` | buffer concatenation | Any |
|
||||
| `toMutable()` | create a mutable copy | MutableBuffer |
|
||||
| `hex` | encode to hex strign | String |
|
||||
| `Buffer.decodeHex(hexStr) | decode hex string | Buffer |
|
||||
| `base64` | encode to base64 (url flavor) (2) | String |
|
||||
| `base64std` | encode to base64 (default vocabulary, filling) | String |
|
||||
| `Buffer.decodeBase64(str)` | decode base64 to new Buffer (2) | Buffer |
|
||||
| `toBitInput()` | create bit input from a byte buffer (3) | |
|
||||
| name | meaning | type |
|
||||
|----------------------------|-----------------------------------------|---------------|
|
||||
| `size` | size | Int |
|
||||
| `decodeUtf8` | decode to String using UTF8 rules | Any |
|
||||
| `+` | buffer concatenation | Any |
|
||||
| `toMutable()` | create a mutable copy | MutableBuffer |
|
||||
| `hex` | encode to hex strign | String |
|
||||
| `Buffer.decodeHex(hexStr) | decode hex string | Buffer |
|
||||
| `base64` | encode to base64 (url flavor) (2) | String |
|
||||
| `Buffer.decodeBase64(str)` | decode base64 to new Buffer (2) | Buffer |
|
||||
| `toBitInput()` | create bit input from a byte buffer (3) | |
|
||||
|
||||
(1)
|
||||
: optimized implementation that override `Iterable` one
|
||||
|
||||
@ -64,18 +64,6 @@ Also, string indexing is Regex-aware, and works like `Regex.find` (_not findall!
|
||||
assert( "cd" == ("abcdef"[ "c.".re ] as RegexMatch).value )
|
||||
>>> void
|
||||
|
||||
Regex replacement is exposed on `String.replace` and `String.replaceFirst`:
|
||||
|
||||
assertEquals( "v#.#.#", "v1.2.3".replace( "\d+".re, "#" ) )
|
||||
assertEquals( "v[1].[2].[3]", "v1.2.3".replace( "(\d+)".re ) { m -> "[" + m[1] + "]" } )
|
||||
assertEquals( "year-04-08", "2026-04-08".replaceFirst( "\d+".re, "year" ) )
|
||||
>>> void
|
||||
|
||||
When `replace` takes a plain `String`, it is treated literally, not as a regex pattern:
|
||||
|
||||
assertEquals( "a-b-c", "a.b.c".replace( ".", "-" ) )
|
||||
>>> void
|
||||
|
||||
|
||||
# Regex class reference
|
||||
|
||||
|
||||
@ -1833,14 +1833,6 @@ Part match:
|
||||
assert( "foo" == ($~ as RegexMatch).value )
|
||||
>>> void
|
||||
|
||||
Replacing text:
|
||||
|
||||
assertEquals("bonono", "banana".replace('a', 'o'))
|
||||
assertEquals("a-b-c", "a.b.c".replace(".", "-")) // string patterns are literal
|
||||
assertEquals("v#.#.#", "v1.2.3".replace("\d+".re, "#"))
|
||||
assertEquals("v[1].[2].[3]", "v1.2.3".replace("(\d+)".re) { m -> "[" + m[1] + "]" })
|
||||
>>> void
|
||||
|
||||
Repeating the fragment:
|
||||
|
||||
assertEquals("hellohello", "hello"*2)
|
||||
@ -1876,8 +1868,6 @@ A typical set of String functions includes:
|
||||
| characters | create [List] of characters (1) |
|
||||
| encodeUtf8() | returns [Buffer] with characters encoded to utf8 |
|
||||
| matches(re) | matches the regular expression (2) |
|
||||
| replace(old, new) | replace all literal or regex matches; regex needs [Regex] |
|
||||
| replaceFirst(old,new)| replace the first literal or regex match |
|
||||
| | |
|
||||
|
||||
(1)
|
||||
|
||||
@ -1,20 +0,0 @@
|
||||
/*
|
||||
* Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
package net.sergeych.lyng.obj
|
||||
|
||||
internal actual fun objListBoundsViolationMessageOrNull(size: Int, index: Int): String? = null
|
||||
@ -3663,11 +3663,7 @@ class CmdGetIndex(
|
||||
val target = frame.storedSlotObj(targetSlot)
|
||||
val index = frame.storedSlotObj(indexSlot)
|
||||
if (target is ObjList && target::class == ObjList::class && index is ObjInt) {
|
||||
val i = index.toInt()
|
||||
objListBoundsViolationMessageOrNull(target.sizeFast(), i)?.let {
|
||||
frame.ensureScope().raiseIndexOutOfBounds(it)
|
||||
}
|
||||
frame.storeObjResult(dst, target.getObjAtFast(i))
|
||||
frame.storeObjResult(dst, target.getObjAtFast(index.toInt()))
|
||||
return
|
||||
}
|
||||
val result = target.getAt(frame.ensureScope(), index)
|
||||
@ -3686,11 +3682,7 @@ class CmdSetIndex(
|
||||
val index = frame.storedSlotObj(indexSlot)
|
||||
val value = frame.slotToObj(valueSlot)
|
||||
if (target is ObjList && target::class == ObjList::class && index is ObjInt) {
|
||||
val i = index.toInt()
|
||||
objListBoundsViolationMessageOrNull(target.sizeFast(), i)?.let {
|
||||
frame.ensureScope().raiseIndexOutOfBounds(it)
|
||||
}
|
||||
target.setObjAtFast(i, value)
|
||||
target.setObjAtFast(index.toInt(), value)
|
||||
return
|
||||
}
|
||||
target.putAt(frame.ensureScope(), index, value)
|
||||
@ -3707,9 +3699,6 @@ class CmdGetIndexInt(
|
||||
val target = frame.storedSlotObj(targetSlot)
|
||||
val index = frame.getInt(indexSlot).toInt()
|
||||
if (target is ObjList && target::class == ObjList::class) {
|
||||
objListBoundsViolationMessageOrNull(target.sizeFast(), index)?.let {
|
||||
frame.ensureScope().raiseIndexOutOfBounds(it)
|
||||
}
|
||||
target.getIntAtFast(index)?.let {
|
||||
frame.setInt(dst, it)
|
||||
return
|
||||
@ -3733,9 +3722,6 @@ class CmdSetIndexInt(
|
||||
val target = frame.storedSlotObj(targetSlot)
|
||||
val index = frame.getInt(indexSlot).toInt()
|
||||
if (target is ObjList && target::class == ObjList::class) {
|
||||
objListBoundsViolationMessageOrNull(target.sizeFast(), index)?.let {
|
||||
frame.ensureScope().raiseIndexOutOfBounds(it)
|
||||
}
|
||||
target.setIntAtFast(index, frame.getInt(valueSlot))
|
||||
return
|
||||
}
|
||||
|
||||
@ -30,7 +30,6 @@ import net.sergeych.lynon.LynonDecoder
|
||||
import net.sergeych.lynon.LynonEncoder
|
||||
import net.sergeych.lynon.LynonType
|
||||
import net.sergeych.mp_tools.decodeBase64Url
|
||||
import net.sergeych.mp_tools.encodeToBase64
|
||||
import net.sergeych.mp_tools.encodeToBase64Url
|
||||
import kotlin.math.min
|
||||
|
||||
@ -40,7 +39,6 @@ open class ObjBuffer(val byteArray: UByteArray) : Obj() {
|
||||
|
||||
val hex by lazy { byteArray.encodeToHex("")}
|
||||
val base64 by lazy { byteArray.toByteArray().encodeToBase64Url()}
|
||||
val base64std by lazy { byteArray.toByteArray().encodeToBase64()}
|
||||
|
||||
fun checkIndex(scope: Scope, index: Obj): Int {
|
||||
if (index !is ObjInt)
|
||||
@ -198,13 +196,6 @@ open class ObjBuffer(val byteArray: UByteArray) : Obj() {
|
||||
moduleName = "lyng.stdlib",
|
||||
getter = { thisAs<ObjBuffer>().base64.toObj() }
|
||||
)
|
||||
addPropertyDoc(
|
||||
name = "base64std",
|
||||
doc = "Base64 standard string representation of the buffer.",
|
||||
type = type("lyng.String"),
|
||||
moduleName = "lyng.stdlib",
|
||||
getter = { thisAs<ObjBuffer>().base64std.toObj() }
|
||||
)
|
||||
addFn("decodeUtf8") {
|
||||
ObjString(
|
||||
thisAs<ObjBuffer>().byteArray.toByteArray().decodeToString()
|
||||
|
||||
@ -171,9 +171,7 @@ open class ObjList(initialList: MutableList<Obj> = mutableListOf()) : Obj() {
|
||||
override suspend fun getAt(scope: Scope, index: Obj): Obj {
|
||||
return when (index) {
|
||||
is ObjInt -> {
|
||||
val i = index.toInt()
|
||||
objListBoundsViolationMessageOrNull(sizeFast(), i)?.let { scope.raiseIndexOutOfBounds(it) }
|
||||
getObjAtFast(i)
|
||||
getObjAtFast(index.toInt())
|
||||
}
|
||||
|
||||
is ObjRange -> {
|
||||
@ -211,9 +209,7 @@ open class ObjList(initialList: MutableList<Obj> = mutableListOf()) : Obj() {
|
||||
}
|
||||
|
||||
open override suspend fun putAt(scope: Scope, index: Obj, newValue: Obj) {
|
||||
val i = index.toInt()
|
||||
objListBoundsViolationMessageOrNull(sizeFast(), i)?.let { scope.raiseIndexOutOfBounds(it) }
|
||||
setObjAtFast(i, newValue)
|
||||
setObjAtFast(index.toInt(), newValue)
|
||||
}
|
||||
|
||||
override suspend fun compareTo(scope: Scope, other: Obj): Int {
|
||||
|
||||
@ -1,20 +0,0 @@
|
||||
/*
|
||||
* Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
package net.sergeych.lyng.obj
|
||||
|
||||
internal expect fun objListBoundsViolationMessageOrNull(size: Int, index: Int): String?
|
||||
@ -21,7 +21,10 @@ import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.JsonElement
|
||||
import kotlinx.serialization.json.JsonPrimitive
|
||||
import net.sergeych.lyng.*
|
||||
import net.sergeych.lyng.PerfFlags
|
||||
import net.sergeych.lyng.Pos
|
||||
import net.sergeych.lyng.RegexCache
|
||||
import net.sergeych.lyng.Scope
|
||||
import net.sergeych.lyng.miniast.*
|
||||
import net.sergeych.lyng.requireScope
|
||||
import net.sergeych.lynon.LynonDecoder
|
||||
@ -129,46 +132,6 @@ data class ObjString(val value: String) : Obj() {
|
||||
return JsonPrimitive(value)
|
||||
}
|
||||
|
||||
private fun replaceLiteralChar(oldValue: Char, newValue: String, firstOnly: Boolean): ObjString {
|
||||
if (!firstOnly) return ObjString(value.replace(oldValue.toString(), newValue))
|
||||
val index = value.indexOf(oldValue)
|
||||
if (index < 0) return this
|
||||
return ObjString(value.substring(0, index) + newValue + value.substring(index + 1))
|
||||
}
|
||||
|
||||
private fun replaceLiteralString(oldValue: String, newValue: String, firstOnly: Boolean): ObjString =
|
||||
ObjString(
|
||||
if (firstOnly) value.replaceFirst(oldValue, newValue)
|
||||
else value.replace(oldValue, newValue)
|
||||
)
|
||||
|
||||
private suspend fun replaceRegex(
|
||||
scope: ScopeFacade,
|
||||
pattern: Regex,
|
||||
firstOnly: Boolean,
|
||||
replacementProvider: suspend (ObjRegexMatch) -> String
|
||||
): ObjString {
|
||||
val firstMatch = pattern.find(value) ?: return this
|
||||
if (firstOnly) {
|
||||
val replacement = replacementProvider(ObjRegexMatch(firstMatch))
|
||||
val start = firstMatch.range.first
|
||||
val endExclusive = start + firstMatch.value.length
|
||||
return ObjString(value.substring(0, start) + replacement + value.substring(endExclusive))
|
||||
}
|
||||
|
||||
val result = StringBuilder(value.length)
|
||||
var lastIndex = 0
|
||||
for (match in pattern.findAll(value)) {
|
||||
val start = match.range.first
|
||||
val endExclusive = start + match.value.length
|
||||
result.append(value, lastIndex, start)
|
||||
result.append(replacementProvider(ObjRegexMatch(match)))
|
||||
lastIndex = endExclusive
|
||||
}
|
||||
result.append(value, lastIndex, value.length)
|
||||
return ObjString(result.toString())
|
||||
}
|
||||
|
||||
companion object {
|
||||
val type = object : ObjClass("String", ObjCollection) {
|
||||
override suspend fun deserialize(scope: Scope, decoder: LynonDecoder, lynonType: LynonType?): Obj =
|
||||
@ -378,190 +341,6 @@ data class ObjString(val value: String) : Obj() {
|
||||
}
|
||||
)
|
||||
}
|
||||
addFnDoc(
|
||||
name = "replace",
|
||||
doc = "Return a copy of this string with all literal or regex matches replaced. String arguments are treated literally; use Regex for regular expressions.",
|
||||
params = listOf(ParamDoc("old"), ParamDoc("new")),
|
||||
returns = type("lyng.String"),
|
||||
moduleName = "lyng.stdlib"
|
||||
) {
|
||||
val source = thisAs<ObjString>()
|
||||
val oldValue = requiredArg<Obj>(0)
|
||||
val newValue = requiredArg<Obj>(1)
|
||||
when (oldValue) {
|
||||
is ObjChar -> {
|
||||
val replacement = when (newValue) {
|
||||
is ObjChar -> newValue.value.toString()
|
||||
is ObjString -> newValue.value
|
||||
else -> raiseIllegalArgument("String.replace(Char, ...) requires Char or String replacement")
|
||||
}
|
||||
source.replaceLiteralChar(oldValue.value, replacement, firstOnly = false)
|
||||
}
|
||||
|
||||
is ObjString -> {
|
||||
val replacement = (newValue as? ObjString)?.value
|
||||
?: raiseIllegalArgument("String.replace(String, ...) requires String replacement")
|
||||
source.replaceLiteralString(oldValue.value, replacement, firstOnly = false)
|
||||
}
|
||||
|
||||
is ObjRegex -> when {
|
||||
newValue is ObjString ->
|
||||
source.replaceRegex(this, oldValue.regex, firstOnly = false) { newValue.value }
|
||||
|
||||
newValue.isInstanceOf("Callable") ->
|
||||
source.replaceRegex(this, oldValue.regex, firstOnly = false) { match ->
|
||||
val transformed = call(newValue, Arguments(match), ObjVoid)
|
||||
(transformed as? ObjString)?.value
|
||||
?: raiseIllegalArgument("String.replace(Regex, transform) callback must return String")
|
||||
}
|
||||
|
||||
else ->
|
||||
raiseIllegalArgument("String.replace(Regex, ...) requires String replacement or a callable")
|
||||
}
|
||||
|
||||
else ->
|
||||
raiseIllegalArgument("String.replace requires Char, String, or Regex as the first argument")
|
||||
}
|
||||
}
|
||||
addFnDoc(
|
||||
name = "replaceFirst",
|
||||
doc = "Return a copy of this string with the first literal or regex match replaced. String arguments are treated literally; use Regex for regular expressions.",
|
||||
params = listOf(ParamDoc("old"), ParamDoc("new")),
|
||||
returns = type("lyng.String"),
|
||||
moduleName = "lyng.stdlib"
|
||||
) {
|
||||
val source = thisAs<ObjString>()
|
||||
val oldValue = requiredArg<Obj>(0)
|
||||
val newValue = requiredArg<Obj>(1)
|
||||
when (oldValue) {
|
||||
is ObjChar -> {
|
||||
val replacement = when (newValue) {
|
||||
is ObjChar -> newValue.value.toString()
|
||||
is ObjString -> newValue.value
|
||||
else -> raiseIllegalArgument("String.replaceFirst(Char, ...) requires Char or String replacement")
|
||||
}
|
||||
source.replaceLiteralChar(oldValue.value, replacement, firstOnly = true)
|
||||
}
|
||||
|
||||
is ObjString -> {
|
||||
val replacement = (newValue as? ObjString)?.value
|
||||
?: raiseIllegalArgument("String.replaceFirst(String, ...) requires String replacement")
|
||||
source.replaceLiteralString(oldValue.value, replacement, firstOnly = true)
|
||||
}
|
||||
|
||||
is ObjRegex -> when {
|
||||
newValue is ObjString ->
|
||||
source.replaceRegex(this, oldValue.regex, firstOnly = true) { newValue.value }
|
||||
|
||||
newValue.isInstanceOf("Callable") ->
|
||||
source.replaceRegex(this, oldValue.regex, firstOnly = true) { match ->
|
||||
val transformed = call(newValue, Arguments(match), ObjVoid)
|
||||
(transformed as? ObjString)?.value
|
||||
?: raiseIllegalArgument("String.replaceFirst(Regex, transform) callback must return String")
|
||||
}
|
||||
|
||||
else ->
|
||||
raiseIllegalArgument("String.replaceFirst(Regex, ...) requires String replacement or a callable")
|
||||
}
|
||||
|
||||
else ->
|
||||
raiseIllegalArgument("String.replaceFirst requires Char, String, or Regex as the first argument")
|
||||
}
|
||||
}
|
||||
BuiltinDocRegistry.module("lyng.stdlib") {
|
||||
classDoc("String", doc = "") {
|
||||
method(
|
||||
name = "replace",
|
||||
doc = "Replace all occurrences of the given character with another character.",
|
||||
params = listOf(
|
||||
ParamDoc("old", type("lyng.Char")),
|
||||
ParamDoc("new", type("lyng.Char"))
|
||||
),
|
||||
returns = type("lyng.String")
|
||||
)
|
||||
method(
|
||||
name = "replace",
|
||||
doc = "Replace all occurrences of the given character with another character or string.",
|
||||
params = listOf(
|
||||
ParamDoc("old", type("lyng.Char")),
|
||||
ParamDoc("new", type("lyng.String"))
|
||||
),
|
||||
returns = type("lyng.String")
|
||||
)
|
||||
method(
|
||||
name = "replace",
|
||||
doc = "Replace all literal occurrences of the given string with another string.",
|
||||
params = listOf(
|
||||
ParamDoc("old", type("lyng.String")),
|
||||
ParamDoc("new", type("lyng.String"))
|
||||
),
|
||||
returns = type("lyng.String")
|
||||
)
|
||||
method(
|
||||
name = "replace",
|
||||
doc = "Replace all regular-expression matches with the given replacement string.",
|
||||
params = listOf(
|
||||
ParamDoc("pattern", type("lyng.Regex")),
|
||||
ParamDoc("new", type("lyng.String"))
|
||||
),
|
||||
returns = type("lyng.String")
|
||||
)
|
||||
method(
|
||||
name = "replace",
|
||||
doc = "Replace all regular-expression matches using the callback result for each RegexMatch.",
|
||||
params = listOf(
|
||||
ParamDoc("pattern", type("lyng.Regex")),
|
||||
ParamDoc("transform", funType(listOf(type("lyng.RegexMatch")), type("lyng.String")))
|
||||
),
|
||||
returns = type("lyng.String")
|
||||
)
|
||||
method(
|
||||
name = "replaceFirst",
|
||||
doc = "Replace the first occurrence of the given character with another character.",
|
||||
params = listOf(
|
||||
ParamDoc("old", type("lyng.Char")),
|
||||
ParamDoc("new", type("lyng.Char"))
|
||||
),
|
||||
returns = type("lyng.String")
|
||||
)
|
||||
method(
|
||||
name = "replaceFirst",
|
||||
doc = "Replace the first occurrence of the given character with another character or string.",
|
||||
params = listOf(
|
||||
ParamDoc("old", type("lyng.Char")),
|
||||
ParamDoc("new", type("lyng.String"))
|
||||
),
|
||||
returns = type("lyng.String")
|
||||
)
|
||||
method(
|
||||
name = "replaceFirst",
|
||||
doc = "Replace the first literal occurrence of the given string with another string.",
|
||||
params = listOf(
|
||||
ParamDoc("old", type("lyng.String")),
|
||||
ParamDoc("new", type("lyng.String"))
|
||||
),
|
||||
returns = type("lyng.String")
|
||||
)
|
||||
method(
|
||||
name = "replaceFirst",
|
||||
doc = "Replace the first regular-expression match with the given replacement string.",
|
||||
params = listOf(
|
||||
ParamDoc("pattern", type("lyng.Regex")),
|
||||
ParamDoc("new", type("lyng.String"))
|
||||
),
|
||||
returns = type("lyng.String")
|
||||
)
|
||||
method(
|
||||
name = "replaceFirst",
|
||||
doc = "Replace the first regular-expression match using the callback result for the RegexMatch.",
|
||||
params = listOf(
|
||||
ParamDoc("pattern", type("lyng.Regex")),
|
||||
ParamDoc("transform", funType(listOf(type("lyng.RegexMatch")), type("lyng.String")))
|
||||
),
|
||||
returns = type("lyng.String")
|
||||
)
|
||||
}
|
||||
}
|
||||
createField(
|
||||
name = "re",
|
||||
initialValue = ObjProperty(
|
||||
|
||||
@ -345,30 +345,9 @@ object LyngLanguageTools {
|
||||
|
||||
fun docAt(analysis: LyngAnalysisResult, offset: Int): LyngSymbolInfo? {
|
||||
StdlibDocsBootstrap.ensure()
|
||||
val target = definitionAt(analysis, offset) ?: return null
|
||||
val mini = analysis.mini
|
||||
val imported = analysis.importedModules
|
||||
val target = definitionAt(analysis, offset) ?: run {
|
||||
val word = DocLookupUtils.wordRangeAt(analysis.text, offset) ?: return null
|
||||
val name = analysis.text.substring(word.first, word.second)
|
||||
val dotPos = DocLookupUtils.prevNonWs(analysis.text, word.first - 1)
|
||||
if (dotPos >= 0 && analysis.text[dotPos] == '.') {
|
||||
val receiverClass = DocLookupUtils.guessReceiverClassViaMini(
|
||||
mini,
|
||||
analysis.text,
|
||||
dotPos,
|
||||
imported,
|
||||
analysis.binding
|
||||
) ?: DocLookupUtils.guessReceiverClass(analysis.text, dotPos, imported, mini)
|
||||
if (receiverClass != null) {
|
||||
LyngSymbolTarget(
|
||||
name = name,
|
||||
kind = SymbolKind.Function,
|
||||
range = TextRange(word.first, word.second),
|
||||
containerName = receiverClass
|
||||
)
|
||||
} else null
|
||||
} else null
|
||||
} ?: return null
|
||||
val name = target.name
|
||||
|
||||
val local = mini?.let { findLocalDecl(it, analysis.text, name, target.range.start) }
|
||||
|
||||
@ -3233,7 +3233,6 @@ class ScriptTest {
|
||||
assertEquals( "hello", b.decodeUtf8() )
|
||||
|
||||
println(b.base64)
|
||||
println(b.base64std)
|
||||
println(b.hex)
|
||||
|
||||
assertEquals( b, Buffer.decodeBase64(b.base64) )
|
||||
@ -4160,30 +4159,6 @@ class ScriptTest {
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testStringReplaceVariants() = runTest {
|
||||
eval(
|
||||
"""
|
||||
assertEquals("bonono", "banana".replace('a', 'o'))
|
||||
assertEquals("bxnxnx", "banana".replace('a', "x"))
|
||||
assertEquals("a-b-c", "a.b.c".replace(".", "-"))
|
||||
assertEquals("bonana", "banana".replaceFirst('a', 'o'))
|
||||
assertEquals("bxnana", "banana".replaceFirst('a', "x"))
|
||||
assertEquals("a-b.c", "a.b.c".replaceFirst(".", "-"))
|
||||
|
||||
assertEquals("foo-#-bar-#", "foo-42-bar-17".replace("\d+".re, "#"))
|
||||
assertEquals("foo-[42]-bar-[17]", "foo-42-bar-17".replace("(\d+)".re) { m ->
|
||||
"[" + m[1] + "]"
|
||||
})
|
||||
assertEquals("foo-[42]-bar-17", "foo-42-bar-17".replaceFirst("(\d+)".re) { m ->
|
||||
"[" + m[1] + "]"
|
||||
})
|
||||
assertEquals("YEAR-04-08", "2026-04-08".replaceFirst("\d+".re, "YEAR"))
|
||||
"""
|
||||
.trimIndent()
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun extensionsMustBeLocalPerScope() = runTest {
|
||||
val scope1 = Script.newScope()
|
||||
|
||||
@ -16,16 +16,8 @@
|
||||
*/
|
||||
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import net.sergeych.lyng.Script
|
||||
import net.sergeych.lyng.eval
|
||||
import net.sergeych.lyng.evalNamed
|
||||
import net.sergeych.lyng.obj.ObjException
|
||||
import net.sergeych.lyng.obj.ObjInstance
|
||||
import net.sergeych.lyng.obj.getLyngExceptionMessage
|
||||
import net.sergeych.lyng.obj.getLyngExceptionStackTrace
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class StdlibTest {
|
||||
@Test
|
||||
@ -381,59 +373,4 @@ class StdlibTest {
|
||||
""".trimIndent()
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testErrorCatching() = runTest {
|
||||
val error = evalNamed("testErrorCatching", """
|
||||
val src = [1,2,3]
|
||||
val d = launch {
|
||||
try {
|
||||
for( i in 0..3 ) src[i]
|
||||
} catch(e) {
|
||||
e
|
||||
}
|
||||
}
|
||||
d.await()
|
||||
""".trimIndent()
|
||||
)
|
||||
|
||||
val scope = when (error) {
|
||||
is ObjException -> error.scope
|
||||
is ObjInstance -> error.instanceScope
|
||||
else -> Script.newScope()
|
||||
}
|
||||
val trace = error.getLyngExceptionStackTrace(scope)
|
||||
val renderedTrace = trace.list.map { it.toString(scope).value }
|
||||
|
||||
assertEquals("Index 3 out of bounds for length 3", error.getLyngExceptionMessage(scope))
|
||||
assertTrue(trace.list.size >= 2, "expected at least await and coroutine frames, got ${trace.list.size}")
|
||||
assertTrue(
|
||||
renderedTrace.all { it.contains("testErrorCatching:") },
|
||||
"unexpected trace entries: $renderedTrace"
|
||||
)
|
||||
assertTrue(
|
||||
renderedTrace.any { it.contains("launch") || it.contains("src[i]") },
|
||||
"trace should include the coroutine body: $renderedTrace"
|
||||
)
|
||||
assertTrue(
|
||||
renderedTrace.any { it.contains("d.await()") },
|
||||
"trace should include await site: $renderedTrace"
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testCatchToIt() = runTest {
|
||||
eval("""
|
||||
var x = 0
|
||||
try {
|
||||
throw "msg1"
|
||||
x = 1
|
||||
}
|
||||
catch {
|
||||
assert(it.message == "msg1")
|
||||
x = 2
|
||||
}
|
||||
assertEquals(2, x)
|
||||
""".trimIndent())
|
||||
}
|
||||
}
|
||||
|
||||
@ -102,27 +102,6 @@ class LyngLanguageToolsTest {
|
||||
assertTrue(local.typeText?.contains("String") == true, "Expected type for local, got ${local.typeText}")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun languageTools_docs_for_builtin_string_replace() = runTest {
|
||||
val code = """
|
||||
val s = "a.b.c".replace(".", "-")
|
||||
""".trimIndent()
|
||||
val res = LyngLanguageTools.analyze(code, "replace_docs.lyng")
|
||||
val replaceOffset = code.indexOf("replace")
|
||||
val doc = LyngLanguageTools.docAt(res, replaceOffset)
|
||||
assertNotNull(doc, "Docs should resolve for built-in String.replace")
|
||||
assertEquals("replace", doc.target.name)
|
||||
assertEquals("String", doc.target.containerName)
|
||||
assertTrue(
|
||||
doc.signature?.startsWith("fun String.replace(") == true,
|
||||
"Expected String.replace signature, got ${doc.signature}"
|
||||
)
|
||||
assertTrue(
|
||||
doc.doc?.summary?.contains("string with all literal or regex matches replaced") == true,
|
||||
"Expected replace summary, got ${doc.doc?.summary}"
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun languageTools_definition_and_usages() = runTest {
|
||||
val code = """
|
||||
|
||||
@ -1,21 +0,0 @@
|
||||
/*
|
||||
* Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
package net.sergeych.lyng.obj
|
||||
|
||||
internal actual fun objListBoundsViolationMessageOrNull(size: Int, index: Int): String? =
|
||||
if (index < 0 || index >= size) "Index $index out of bounds for length $size" else null
|
||||
@ -1,20 +0,0 @@
|
||||
/*
|
||||
* Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
package net.sergeych.lyng.obj
|
||||
|
||||
internal actual fun objListBoundsViolationMessageOrNull(size: Int, index: Int): String? = null
|
||||
@ -18,6 +18,7 @@
|
||||
package net.sergeych.lyng.miniast
|
||||
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlin.test.Ignore
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertFalse
|
||||
import kotlin.test.assertNotNull
|
||||
@ -95,14 +96,6 @@ class CompletionEngineLightTest {
|
||||
val ns = names(items)
|
||||
assertTrue(ns.isNotEmpty(), "String members should be suggested")
|
||||
assertFalse(ns.contains("Path"))
|
||||
assertTrue(ns.contains("replace"), "Expected String.replace in completion items, got: $ns")
|
||||
assertTrue(ns.contains("replaceFirst"), "Expected String.replaceFirst in completion items, got: $ns")
|
||||
val replaceItem = items.firstOrNull { it.name == "replace" }
|
||||
assertNotNull(replaceItem, "Expected to find replace in String members")
|
||||
assertTrue(
|
||||
replaceItem.tailText?.contains("overloads") == true,
|
||||
"Expected replace completion to show overloads, got tailText=${replaceItem.tailText}"
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@ -1,20 +0,0 @@
|
||||
/*
|
||||
* Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
package net.sergeych.lyng.obj
|
||||
|
||||
internal actual fun objListBoundsViolationMessageOrNull(size: Int, index: Int): String? = null
|
||||
@ -1,21 +0,0 @@
|
||||
/*
|
||||
* Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
package net.sergeych.lyng.obj
|
||||
|
||||
internal actual fun objListBoundsViolationMessageOrNull(size: Int, index: Int): String? =
|
||||
if (index < 0 || index >= size) "Index $index out of bounds for length $size" else null
|
||||
@ -20,10 +20,6 @@ import kotlinx.browser.window
|
||||
import org.jetbrains.compose.web.dom.*
|
||||
import org.w3c.dom.HTMLElement
|
||||
|
||||
private const val DESKTOP_TOC_BREAKPOINT_PX = 992
|
||||
|
||||
fun isDesktopTocLayout(viewportWidthPx: Int): Boolean = viewportWidthPx >= DESKTOP_TOC_BREAKPOINT_PX
|
||||
|
||||
@Composable
|
||||
fun App() {
|
||||
var route by remember { mutableStateOf(currentRoute()) }
|
||||
@ -33,8 +29,6 @@ fun App() {
|
||||
var activeTocId by remember { mutableStateOf<String?>(null) }
|
||||
var contentEl by remember { mutableStateOf<HTMLElement?>(null) }
|
||||
var navEl by remember { mutableStateOf<HTMLElement?>(null) }
|
||||
var mobileTocExpanded by remember { mutableStateOf(false) }
|
||||
var isDesktopToc by remember { mutableStateOf(isDesktopTocLayout(window.innerWidth)) }
|
||||
val isDocsRoute = route.startsWith("docs/")
|
||||
val docKey = stripFragment(route)
|
||||
|
||||
@ -56,11 +50,7 @@ fun App() {
|
||||
}
|
||||
|
||||
DisposableEffect(Unit) {
|
||||
val handler: (org.w3c.dom.events.Event) -> Unit = {
|
||||
updateNavbarOffsetVar()
|
||||
isDesktopToc = isDesktopTocLayout(window.innerWidth)
|
||||
}
|
||||
isDesktopToc = isDesktopTocLayout(window.innerWidth)
|
||||
val handler: (org.w3c.dom.events.Event) -> Unit = { updateNavbarOffsetVar() }
|
||||
window.addEventListener("resize", handler)
|
||||
onDispose { window.removeEventListener("resize", handler) }
|
||||
}
|
||||
@ -71,18 +61,13 @@ fun App() {
|
||||
onDispose { window.removeEventListener("hashchange", listener) }
|
||||
}
|
||||
|
||||
LaunchedEffect(activeTocId, isDesktopToc) {
|
||||
if (!isDesktopToc) return@LaunchedEffect
|
||||
LaunchedEffect(activeTocId) {
|
||||
val activeId = activeTocId ?: return@LaunchedEffect
|
||||
val nav = navEl ?: return@LaunchedEffect
|
||||
val activeLink = nav.querySelector("a[data-toc-id=\"$activeId\"]") as? HTMLElement
|
||||
activeLink?.scrollIntoView(js("({block: 'nearest', behavior: 'smooth'})"))
|
||||
}
|
||||
|
||||
LaunchedEffect(docKey) {
|
||||
mobileTocExpanded = false
|
||||
}
|
||||
|
||||
PageTemplate(title = when {
|
||||
isDocsRoute -> null
|
||||
route.startsWith("authors") -> "Authors"
|
||||
@ -93,29 +78,40 @@ fun App() {
|
||||
Div({ classes("row", "gy-4") }) {
|
||||
if (isDocsRoute) {
|
||||
Div({ classes("col-12", "col-lg-3") }) {
|
||||
if (toc.isNotEmpty() && !isDesktopToc) {
|
||||
Button(attrs = {
|
||||
classes("btn", "btn-outline-secondary", "w-100", "mb-3", "d-lg-none")
|
||||
attr("type", "button")
|
||||
attr("aria-expanded", mobileTocExpanded.toString())
|
||||
attr("aria-controls", "docs-toc-nav")
|
||||
onClick { mobileTocExpanded = !mobileTocExpanded }
|
||||
}) {
|
||||
Text(if (mobileTocExpanded) "Hide contents" else "Show contents")
|
||||
Nav({
|
||||
classes("position-sticky")
|
||||
attr("style", "top: calc(var(--navbar-offset) + 1rem); max-height: calc(100vh - var(--navbar-offset) - 2rem); overflow-y: auto;")
|
||||
ref {
|
||||
navEl = it
|
||||
onDispose { navEl = null }
|
||||
}
|
||||
}) {
|
||||
H2({ classes("h6", "text-uppercase", "text-muted") }) { Text("On this page") }
|
||||
Ul({ classes("list-unstyled") }) {
|
||||
toc.forEach { item ->
|
||||
Li({ classes("mb-1") }) {
|
||||
val pad = when (item.level) { 1 -> "0"; 2 -> "0.75rem"; else -> "1.5rem" }
|
||||
val routeNoFrag = route.substringBefore('#')
|
||||
val tocHref = "#/$routeNoFrag#${item.id}"
|
||||
A(attrs = {
|
||||
attr("href", tocHref)
|
||||
attr("data-toc-id", item.id)
|
||||
attr("style", "padding-left: $pad")
|
||||
classes("link-body-emphasis", "text-decoration-none")
|
||||
if (activeTocId == item.id) {
|
||||
classes("fw-semibold", "text-primary")
|
||||
attr("aria-current", "true")
|
||||
}
|
||||
onClick {
|
||||
it.preventDefault()
|
||||
window.location.hash = tocHref
|
||||
contentEl?.ownerDocument?.getElementById(item.id)
|
||||
?.let { (it as? HTMLElement)?.scrollIntoView() }
|
||||
}
|
||||
}) { Text(item.title) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (toc.isNotEmpty() && (isDesktopToc || mobileTocExpanded)) {
|
||||
TocNav(
|
||||
toc = toc,
|
||||
route = route,
|
||||
activeTocId = activeTocId,
|
||||
isDesktopToc = isDesktopToc,
|
||||
contentEl = contentEl,
|
||||
onNavigate = {
|
||||
if (!isDesktopToc) mobileTocExpanded = false
|
||||
},
|
||||
onNavEl = { navEl = it }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -145,59 +141,3 @@ fun App() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TocNav(
|
||||
toc: List<TocItem>,
|
||||
route: String,
|
||||
activeTocId: String?,
|
||||
isDesktopToc: Boolean,
|
||||
contentEl: HTMLElement?,
|
||||
onNavigate: () -> Unit,
|
||||
onNavEl: (HTMLElement?) -> Unit,
|
||||
) {
|
||||
Nav({
|
||||
id("docs-toc-nav")
|
||||
classes(if (isDesktopToc) "position-sticky" else "docs-mobile-toc", "mb-3", "mb-lg-0")
|
||||
attr(
|
||||
"style",
|
||||
if (isDesktopToc) {
|
||||
"top: calc(var(--navbar-offset) + 1rem); max-height: calc(100vh - var(--navbar-offset) - 2rem); overflow-y: auto;"
|
||||
} else {
|
||||
"max-height: min(50vh, 24rem); overflow-y: auto;"
|
||||
}
|
||||
)
|
||||
ref {
|
||||
onNavEl(it)
|
||||
onDispose { onNavEl(null) }
|
||||
}
|
||||
}) {
|
||||
H2({ classes("h6", "text-uppercase", "text-muted", "mb-2") }) { Text("On this page") }
|
||||
Ul({ classes("list-unstyled", "mb-0") }) {
|
||||
toc.forEach { item ->
|
||||
Li({ classes("mb-1") }) {
|
||||
val pad = when (item.level) { 1 -> "0"; 2 -> "0.75rem"; else -> "1.5rem" }
|
||||
val routeNoFrag = route.substringBefore('#')
|
||||
val tocHref = "#/$routeNoFrag#${item.id}"
|
||||
A(attrs = {
|
||||
attr("href", tocHref)
|
||||
attr("data-toc-id", item.id)
|
||||
attr("style", "display: block; padding-left: $pad")
|
||||
classes("link-body-emphasis", "text-decoration-none")
|
||||
if (activeTocId == item.id) {
|
||||
classes("fw-semibold", "text-primary")
|
||||
attr("aria-current", "true")
|
||||
}
|
||||
onClick {
|
||||
it.preventDefault()
|
||||
onNavigate()
|
||||
window.location.hash = tocHref
|
||||
contentEl?.ownerDocument?.getElementById(item.id)
|
||||
?.let { heading -> (heading as? HTMLElement)?.scrollIntoView() }
|
||||
}
|
||||
}) { Text(item.title) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -180,34 +180,17 @@ fun ensureDocsLayoutStyles() {
|
||||
.markdown-body h1:first-child {
|
||||
margin-top: 0 !important;
|
||||
}
|
||||
.docs-mobile-toc {
|
||||
position: static !important;
|
||||
padding: 0.875rem 1rem;
|
||||
border: 1px solid rgba(128, 128, 128, 0.2);
|
||||
border-radius: 0.75rem;
|
||||
background: var(--bs-body-bg, #fff);
|
||||
}
|
||||
/* Hide scrollbar for the TOC nav but allow scrolling */
|
||||
nav.position-sticky::-webkit-scrollbar {
|
||||
width: 4px;
|
||||
}
|
||||
.docs-mobile-toc::-webkit-scrollbar {
|
||||
width: 4px;
|
||||
}
|
||||
nav.position-sticky::-webkit-scrollbar-thumb {
|
||||
background: rgba(128,128,128,0.2);
|
||||
border-radius: 4px;
|
||||
}
|
||||
.docs-mobile-toc::-webkit-scrollbar-thumb {
|
||||
background: rgba(128,128,128,0.2);
|
||||
border-radius: 4px;
|
||||
}
|
||||
nav.position-sticky:hover::-webkit-scrollbar-thumb {
|
||||
background: rgba(128,128,128,0.5);
|
||||
}
|
||||
.docs-mobile-toc:hover::-webkit-scrollbar-thumb {
|
||||
background: rgba(128,128,128,0.5);
|
||||
}
|
||||
"""
|
||||
.trimIndent()
|
||||
)
|
||||
|
||||
@ -1,33 +0,0 @@
|
||||
/*
|
||||
* Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertFalse
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class AppResponsiveTocTest {
|
||||
@Test
|
||||
fun tocIsCollapsedBelowLgBreakpoint() {
|
||||
assertFalse(isDesktopTocLayout(991))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun tocStaysDesktopAtLgBreakpointAndAbove() {
|
||||
assertTrue(isDesktopTocLayout(992))
|
||||
assertTrue(isDesktopTocLayout(1280))
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user