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
|
## Members
|
||||||
|
|
||||||
| name | meaning | type |
|
| name | meaning | type |
|
||||||
|----------------------------|------------------------------------------------|---------------|
|
|----------------------------|-----------------------------------------|---------------|
|
||||||
| `size` | size | Int |
|
| `size` | size | Int |
|
||||||
| `decodeUtf8` | decode to String using UTF8 rules | Any |
|
| `decodeUtf8` | decode to String using UTF8 rules | Any |
|
||||||
| `+` | buffer concatenation | Any |
|
| `+` | buffer concatenation | Any |
|
||||||
| `toMutable()` | create a mutable copy | MutableBuffer |
|
| `toMutable()` | create a mutable copy | MutableBuffer |
|
||||||
| `hex` | encode to hex strign | String |
|
| `hex` | encode to hex strign | String |
|
||||||
| `Buffer.decodeHex(hexStr) | decode hex string | Buffer |
|
| `Buffer.decodeHex(hexStr) | decode hex string | Buffer |
|
||||||
| `base64` | encode to base64 (url flavor) (2) | String |
|
| `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 |
|
||||||
| `Buffer.decodeBase64(str)` | decode base64 to new Buffer (2) | Buffer |
|
| `toBitInput()` | create bit input from a byte buffer (3) | |
|
||||||
| `toBitInput()` | create bit input from a byte buffer (3) | |
|
|
||||||
|
|
||||||
(1)
|
(1)
|
||||||
: optimized implementation that override `Iterable` one
|
: 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 )
|
assert( "cd" == ("abcdef"[ "c.".re ] as RegexMatch).value )
|
||||||
>>> void
|
>>> 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
|
# Regex class reference
|
||||||
|
|
||||||
|
|||||||
@ -1833,14 +1833,6 @@ Part match:
|
|||||||
assert( "foo" == ($~ as RegexMatch).value )
|
assert( "foo" == ($~ as RegexMatch).value )
|
||||||
>>> void
|
>>> 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:
|
Repeating the fragment:
|
||||||
|
|
||||||
assertEquals("hellohello", "hello"*2)
|
assertEquals("hellohello", "hello"*2)
|
||||||
@ -1876,8 +1868,6 @@ A typical set of String functions includes:
|
|||||||
| characters | create [List] of characters (1) |
|
| characters | create [List] of characters (1) |
|
||||||
| encodeUtf8() | returns [Buffer] with characters encoded to utf8 |
|
| encodeUtf8() | returns [Buffer] with characters encoded to utf8 |
|
||||||
| matches(re) | matches the regular expression (2) |
|
| 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)
|
||||||
|
|||||||
@ -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 target = frame.storedSlotObj(targetSlot)
|
||||||
val index = frame.storedSlotObj(indexSlot)
|
val index = frame.storedSlotObj(indexSlot)
|
||||||
if (target is ObjList && target::class == ObjList::class && index is ObjInt) {
|
if (target is ObjList && target::class == ObjList::class && index is ObjInt) {
|
||||||
val i = index.toInt()
|
frame.storeObjResult(dst, target.getObjAtFast(index.toInt()))
|
||||||
objListBoundsViolationMessageOrNull(target.sizeFast(), i)?.let {
|
|
||||||
frame.ensureScope().raiseIndexOutOfBounds(it)
|
|
||||||
}
|
|
||||||
frame.storeObjResult(dst, target.getObjAtFast(i))
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
val result = target.getAt(frame.ensureScope(), index)
|
val result = target.getAt(frame.ensureScope(), index)
|
||||||
@ -3686,11 +3682,7 @@ class CmdSetIndex(
|
|||||||
val index = frame.storedSlotObj(indexSlot)
|
val index = frame.storedSlotObj(indexSlot)
|
||||||
val value = frame.slotToObj(valueSlot)
|
val value = frame.slotToObj(valueSlot)
|
||||||
if (target is ObjList && target::class == ObjList::class && index is ObjInt) {
|
if (target is ObjList && target::class == ObjList::class && index is ObjInt) {
|
||||||
val i = index.toInt()
|
target.setObjAtFast(index.toInt(), value)
|
||||||
objListBoundsViolationMessageOrNull(target.sizeFast(), i)?.let {
|
|
||||||
frame.ensureScope().raiseIndexOutOfBounds(it)
|
|
||||||
}
|
|
||||||
target.setObjAtFast(i, value)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
target.putAt(frame.ensureScope(), index, value)
|
target.putAt(frame.ensureScope(), index, value)
|
||||||
@ -3707,9 +3699,6 @@ class CmdGetIndexInt(
|
|||||||
val target = frame.storedSlotObj(targetSlot)
|
val target = frame.storedSlotObj(targetSlot)
|
||||||
val index = frame.getInt(indexSlot).toInt()
|
val index = frame.getInt(indexSlot).toInt()
|
||||||
if (target is ObjList && target::class == ObjList::class) {
|
if (target is ObjList && target::class == ObjList::class) {
|
||||||
objListBoundsViolationMessageOrNull(target.sizeFast(), index)?.let {
|
|
||||||
frame.ensureScope().raiseIndexOutOfBounds(it)
|
|
||||||
}
|
|
||||||
target.getIntAtFast(index)?.let {
|
target.getIntAtFast(index)?.let {
|
||||||
frame.setInt(dst, it)
|
frame.setInt(dst, it)
|
||||||
return
|
return
|
||||||
@ -3733,9 +3722,6 @@ class CmdSetIndexInt(
|
|||||||
val target = frame.storedSlotObj(targetSlot)
|
val target = frame.storedSlotObj(targetSlot)
|
||||||
val index = frame.getInt(indexSlot).toInt()
|
val index = frame.getInt(indexSlot).toInt()
|
||||||
if (target is ObjList && target::class == ObjList::class) {
|
if (target is ObjList && target::class == ObjList::class) {
|
||||||
objListBoundsViolationMessageOrNull(target.sizeFast(), index)?.let {
|
|
||||||
frame.ensureScope().raiseIndexOutOfBounds(it)
|
|
||||||
}
|
|
||||||
target.setIntAtFast(index, frame.getInt(valueSlot))
|
target.setIntAtFast(index, frame.getInt(valueSlot))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@ -30,7 +30,6 @@ import net.sergeych.lynon.LynonDecoder
|
|||||||
import net.sergeych.lynon.LynonEncoder
|
import net.sergeych.lynon.LynonEncoder
|
||||||
import net.sergeych.lynon.LynonType
|
import net.sergeych.lynon.LynonType
|
||||||
import net.sergeych.mp_tools.decodeBase64Url
|
import net.sergeych.mp_tools.decodeBase64Url
|
||||||
import net.sergeych.mp_tools.encodeToBase64
|
|
||||||
import net.sergeych.mp_tools.encodeToBase64Url
|
import net.sergeych.mp_tools.encodeToBase64Url
|
||||||
import kotlin.math.min
|
import kotlin.math.min
|
||||||
|
|
||||||
@ -40,7 +39,6 @@ open class ObjBuffer(val byteArray: UByteArray) : Obj() {
|
|||||||
|
|
||||||
val hex by lazy { byteArray.encodeToHex("")}
|
val hex by lazy { byteArray.encodeToHex("")}
|
||||||
val base64 by lazy { byteArray.toByteArray().encodeToBase64Url()}
|
val base64 by lazy { byteArray.toByteArray().encodeToBase64Url()}
|
||||||
val base64std by lazy { byteArray.toByteArray().encodeToBase64()}
|
|
||||||
|
|
||||||
fun checkIndex(scope: Scope, index: Obj): Int {
|
fun checkIndex(scope: Scope, index: Obj): Int {
|
||||||
if (index !is ObjInt)
|
if (index !is ObjInt)
|
||||||
@ -198,13 +196,6 @@ open class ObjBuffer(val byteArray: UByteArray) : Obj() {
|
|||||||
moduleName = "lyng.stdlib",
|
moduleName = "lyng.stdlib",
|
||||||
getter = { thisAs<ObjBuffer>().base64.toObj() }
|
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") {
|
addFn("decodeUtf8") {
|
||||||
ObjString(
|
ObjString(
|
||||||
thisAs<ObjBuffer>().byteArray.toByteArray().decodeToString()
|
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 {
|
override suspend fun getAt(scope: Scope, index: Obj): Obj {
|
||||||
return when (index) {
|
return when (index) {
|
||||||
is ObjInt -> {
|
is ObjInt -> {
|
||||||
val i = index.toInt()
|
getObjAtFast(index.toInt())
|
||||||
objListBoundsViolationMessageOrNull(sizeFast(), i)?.let { scope.raiseIndexOutOfBounds(it) }
|
|
||||||
getObjAtFast(i)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
is ObjRange -> {
|
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) {
|
open override suspend fun putAt(scope: Scope, index: Obj, newValue: Obj) {
|
||||||
val i = index.toInt()
|
setObjAtFast(index.toInt(), newValue)
|
||||||
objListBoundsViolationMessageOrNull(sizeFast(), i)?.let { scope.raiseIndexOutOfBounds(it) }
|
|
||||||
setObjAtFast(i, newValue)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun compareTo(scope: Scope, other: Obj): Int {
|
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.Serializable
|
||||||
import kotlinx.serialization.json.JsonElement
|
import kotlinx.serialization.json.JsonElement
|
||||||
import kotlinx.serialization.json.JsonPrimitive
|
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.miniast.*
|
||||||
import net.sergeych.lyng.requireScope
|
import net.sergeych.lyng.requireScope
|
||||||
import net.sergeych.lynon.LynonDecoder
|
import net.sergeych.lynon.LynonDecoder
|
||||||
@ -129,46 +132,6 @@ data class ObjString(val value: String) : Obj() {
|
|||||||
return JsonPrimitive(value)
|
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 {
|
companion object {
|
||||||
val type = object : ObjClass("String", ObjCollection) {
|
val type = object : ObjClass("String", ObjCollection) {
|
||||||
override suspend fun deserialize(scope: Scope, decoder: LynonDecoder, lynonType: LynonType?): Obj =
|
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(
|
createField(
|
||||||
name = "re",
|
name = "re",
|
||||||
initialValue = ObjProperty(
|
initialValue = ObjProperty(
|
||||||
|
|||||||
@ -345,30 +345,9 @@ object LyngLanguageTools {
|
|||||||
|
|
||||||
fun docAt(analysis: LyngAnalysisResult, offset: Int): LyngSymbolInfo? {
|
fun docAt(analysis: LyngAnalysisResult, offset: Int): LyngSymbolInfo? {
|
||||||
StdlibDocsBootstrap.ensure()
|
StdlibDocsBootstrap.ensure()
|
||||||
|
val target = definitionAt(analysis, offset) ?: return null
|
||||||
val mini = analysis.mini
|
val mini = analysis.mini
|
||||||
val imported = analysis.importedModules
|
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 name = target.name
|
||||||
|
|
||||||
val local = mini?.let { findLocalDecl(it, analysis.text, name, target.range.start) }
|
val local = mini?.let { findLocalDecl(it, analysis.text, name, target.range.start) }
|
||||||
|
|||||||
@ -3233,7 +3233,6 @@ class ScriptTest {
|
|||||||
assertEquals( "hello", b.decodeUtf8() )
|
assertEquals( "hello", b.decodeUtf8() )
|
||||||
|
|
||||||
println(b.base64)
|
println(b.base64)
|
||||||
println(b.base64std)
|
|
||||||
println(b.hex)
|
println(b.hex)
|
||||||
|
|
||||||
assertEquals( b, Buffer.decodeBase64(b.base64) )
|
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
|
@Test
|
||||||
fun extensionsMustBeLocalPerScope() = runTest {
|
fun extensionsMustBeLocalPerScope() = runTest {
|
||||||
val scope1 = Script.newScope()
|
val scope1 = Script.newScope()
|
||||||
|
|||||||
@ -16,16 +16,8 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import net.sergeych.lyng.Script
|
|
||||||
import net.sergeych.lyng.eval
|
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.Test
|
||||||
import kotlin.test.assertEquals
|
|
||||||
import kotlin.test.assertTrue
|
|
||||||
|
|
||||||
class StdlibTest {
|
class StdlibTest {
|
||||||
@Test
|
@Test
|
||||||
@ -381,59 +373,4 @@ class StdlibTest {
|
|||||||
""".trimIndent()
|
""".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}")
|
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
|
@Test
|
||||||
fun languageTools_definition_and_usages() = runTest {
|
fun languageTools_definition_and_usages() = runTest {
|
||||||
val code = """
|
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
|
package net.sergeych.lyng.miniast
|
||||||
|
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
|
import kotlin.test.Ignore
|
||||||
import kotlin.test.Test
|
import kotlin.test.Test
|
||||||
import kotlin.test.assertFalse
|
import kotlin.test.assertFalse
|
||||||
import kotlin.test.assertNotNull
|
import kotlin.test.assertNotNull
|
||||||
@ -95,14 +96,6 @@ class CompletionEngineLightTest {
|
|||||||
val ns = names(items)
|
val ns = names(items)
|
||||||
assertTrue(ns.isNotEmpty(), "String members should be suggested")
|
assertTrue(ns.isNotEmpty(), "String members should be suggested")
|
||||||
assertFalse(ns.contains("Path"))
|
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
|
@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.jetbrains.compose.web.dom.*
|
||||||
import org.w3c.dom.HTMLElement
|
import org.w3c.dom.HTMLElement
|
||||||
|
|
||||||
private const val DESKTOP_TOC_BREAKPOINT_PX = 992
|
|
||||||
|
|
||||||
fun isDesktopTocLayout(viewportWidthPx: Int): Boolean = viewportWidthPx >= DESKTOP_TOC_BREAKPOINT_PX
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun App() {
|
fun App() {
|
||||||
var route by remember { mutableStateOf(currentRoute()) }
|
var route by remember { mutableStateOf(currentRoute()) }
|
||||||
@ -33,8 +29,6 @@ fun App() {
|
|||||||
var activeTocId by remember { mutableStateOf<String?>(null) }
|
var activeTocId by remember { mutableStateOf<String?>(null) }
|
||||||
var contentEl by remember { mutableStateOf<HTMLElement?>(null) }
|
var contentEl by remember { mutableStateOf<HTMLElement?>(null) }
|
||||||
var navEl 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 isDocsRoute = route.startsWith("docs/")
|
||||||
val docKey = stripFragment(route)
|
val docKey = stripFragment(route)
|
||||||
|
|
||||||
@ -56,11 +50,7 @@ fun App() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
DisposableEffect(Unit) {
|
DisposableEffect(Unit) {
|
||||||
val handler: (org.w3c.dom.events.Event) -> Unit = {
|
val handler: (org.w3c.dom.events.Event) -> Unit = { updateNavbarOffsetVar() }
|
||||||
updateNavbarOffsetVar()
|
|
||||||
isDesktopToc = isDesktopTocLayout(window.innerWidth)
|
|
||||||
}
|
|
||||||
isDesktopToc = isDesktopTocLayout(window.innerWidth)
|
|
||||||
window.addEventListener("resize", handler)
|
window.addEventListener("resize", handler)
|
||||||
onDispose { window.removeEventListener("resize", handler) }
|
onDispose { window.removeEventListener("resize", handler) }
|
||||||
}
|
}
|
||||||
@ -71,18 +61,13 @@ fun App() {
|
|||||||
onDispose { window.removeEventListener("hashchange", listener) }
|
onDispose { window.removeEventListener("hashchange", listener) }
|
||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(activeTocId, isDesktopToc) {
|
LaunchedEffect(activeTocId) {
|
||||||
if (!isDesktopToc) return@LaunchedEffect
|
|
||||||
val activeId = activeTocId ?: return@LaunchedEffect
|
val activeId = activeTocId ?: return@LaunchedEffect
|
||||||
val nav = navEl ?: return@LaunchedEffect
|
val nav = navEl ?: return@LaunchedEffect
|
||||||
val activeLink = nav.querySelector("a[data-toc-id=\"$activeId\"]") as? HTMLElement
|
val activeLink = nav.querySelector("a[data-toc-id=\"$activeId\"]") as? HTMLElement
|
||||||
activeLink?.scrollIntoView(js("({block: 'nearest', behavior: 'smooth'})"))
|
activeLink?.scrollIntoView(js("({block: 'nearest', behavior: 'smooth'})"))
|
||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(docKey) {
|
|
||||||
mobileTocExpanded = false
|
|
||||||
}
|
|
||||||
|
|
||||||
PageTemplate(title = when {
|
PageTemplate(title = when {
|
||||||
isDocsRoute -> null
|
isDocsRoute -> null
|
||||||
route.startsWith("authors") -> "Authors"
|
route.startsWith("authors") -> "Authors"
|
||||||
@ -93,29 +78,40 @@ fun App() {
|
|||||||
Div({ classes("row", "gy-4") }) {
|
Div({ classes("row", "gy-4") }) {
|
||||||
if (isDocsRoute) {
|
if (isDocsRoute) {
|
||||||
Div({ classes("col-12", "col-lg-3") }) {
|
Div({ classes("col-12", "col-lg-3") }) {
|
||||||
if (toc.isNotEmpty() && !isDesktopToc) {
|
Nav({
|
||||||
Button(attrs = {
|
classes("position-sticky")
|
||||||
classes("btn", "btn-outline-secondary", "w-100", "mb-3", "d-lg-none")
|
attr("style", "top: calc(var(--navbar-offset) + 1rem); max-height: calc(100vh - var(--navbar-offset) - 2rem); overflow-y: auto;")
|
||||||
attr("type", "button")
|
ref {
|
||||||
attr("aria-expanded", mobileTocExpanded.toString())
|
navEl = it
|
||||||
attr("aria-controls", "docs-toc-nav")
|
onDispose { navEl = null }
|
||||||
onClick { mobileTocExpanded = !mobileTocExpanded }
|
}
|
||||||
}) {
|
}) {
|
||||||
Text(if (mobileTocExpanded) "Hide contents" else "Show contents")
|
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 {
|
.markdown-body h1:first-child {
|
||||||
margin-top: 0 !important;
|
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 */
|
/* Hide scrollbar for the TOC nav but allow scrolling */
|
||||||
nav.position-sticky::-webkit-scrollbar {
|
nav.position-sticky::-webkit-scrollbar {
|
||||||
width: 4px;
|
width: 4px;
|
||||||
}
|
}
|
||||||
.docs-mobile-toc::-webkit-scrollbar {
|
|
||||||
width: 4px;
|
|
||||||
}
|
|
||||||
nav.position-sticky::-webkit-scrollbar-thumb {
|
nav.position-sticky::-webkit-scrollbar-thumb {
|
||||||
background: rgba(128,128,128,0.2);
|
background: rgba(128,128,128,0.2);
|
||||||
border-radius: 4px;
|
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 {
|
nav.position-sticky:hover::-webkit-scrollbar-thumb {
|
||||||
background: rgba(128,128,128,0.5);
|
background: rgba(128,128,128,0.5);
|
||||||
}
|
}
|
||||||
.docs-mobile-toc:hover::-webkit-scrollbar-thumb {
|
|
||||||
background: rgba(128,128,128,0.5);
|
|
||||||
}
|
|
||||||
"""
|
"""
|
||||||
.trimIndent()
|
.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