map literals
This commit is contained in:
parent
cb9df79ce3
commit
d118d29429
81
docs/Map.md
81
docs/Map.md
@ -1,19 +1,22 @@
|
|||||||
# Map
|
# Map
|
||||||
|
|
||||||
Map is a mutable collection of key-value pars, where keys are unique. Maps could be created with
|
Map is a mutable collection of key-value pairs, where keys are unique. You can create maps in two ways:
|
||||||
constructor or `.toMap` methods. When constructing from a list, each list item must be a [Collection] with exactly 2 elements, for example, a [List].
|
- with the constructor `Map(...)` or `.toMap()` helpers; and
|
||||||
|
- with map literals using braces: `{ "key": value, id: expr, id: }`.
|
||||||
|
|
||||||
|
When constructing from a list, each list item must be a [Collection] with exactly 2 elements, for example, a [List].
|
||||||
|
|
||||||
Important thing is that maps can't contain `null`: it is used to return from missing elements.
|
Important thing is that maps can't contain `null`: it is used to return from missing elements.
|
||||||
|
|
||||||
Constructed map instance is of class `Map` and implements `Collection` (and therefore `Iterable`)
|
Constructed map instance is of class `Map` and implements `Collection` (and therefore `Iterable`).
|
||||||
|
|
||||||
val map = Map( "foo" => 1, "bar" => "buzz" )
|
val oldForm = Map( "foo" => 1, "bar" => "buzz" )
|
||||||
assert(map is Map)
|
assert(oldForm is Map)
|
||||||
assert(map.size == 2)
|
assert(oldForm.size == 2)
|
||||||
assert(map is Iterable)
|
assert(oldForm is Iterable)
|
||||||
>>> void
|
>>> void
|
||||||
|
|
||||||
Notice usage of the `=>` operator that creates `MapEntry`, which implements also [Collection] of
|
Notice usage of the `=>` operator that creates `MapEntry`, which also implements [Collection] of
|
||||||
two items, first, at index zero, is a key, second, at index 1, is the value. You can use lists too.
|
two items, first, at index zero, is a key, second, at index 1, is the value. You can use lists too.
|
||||||
Map keys could be any objects (hashable, e.g. with reasonable hashCode, most of standard types are). You can access elements with indexing operator:
|
Map keys could be any objects (hashable, e.g. with reasonable hashCode, most of standard types are). You can access elements with indexing operator:
|
||||||
|
|
||||||
@ -29,6 +32,36 @@ Map keys could be any objects (hashable, e.g. with reasonable hashCode, most of
|
|||||||
assert( map["foo"] == -1)
|
assert( map["foo"] == -1)
|
||||||
>>> void
|
>>> void
|
||||||
|
|
||||||
|
## Map literals { ... }
|
||||||
|
|
||||||
|
Lyng supports JavaScript-like map literals. Keys can be string literals or identifiers, and there is a handy identifier shorthand:
|
||||||
|
|
||||||
|
- String key: `{ "a": 1 }`
|
||||||
|
- Identifier key: `{ foo: 2 }` is the same as `{ "foo": 2 }`
|
||||||
|
- Identifier shorthand: `{ foo: }` is the same as `{ "foo": foo }`
|
||||||
|
|
||||||
|
Access uses brackets: `m["a"]`.
|
||||||
|
|
||||||
|
val x = 10
|
||||||
|
val y = 10
|
||||||
|
val m = { "a": 1, x: x * 2, y: }
|
||||||
|
assertEquals(1, m["a"]) // string-literal key
|
||||||
|
assertEquals(20, m["x"]) // identifier key
|
||||||
|
assertEquals(10, m["y"]) // identifier shorthand expands to y: y
|
||||||
|
>>> void
|
||||||
|
|
||||||
|
Trailing commas are allowed for nicer diffs and multiline formatting:
|
||||||
|
|
||||||
|
val m = {
|
||||||
|
"a": 1,
|
||||||
|
b: 2,
|
||||||
|
}
|
||||||
|
assertEquals(1, m["a"])
|
||||||
|
assertEquals(2, m["b"])
|
||||||
|
>>> void
|
||||||
|
|
||||||
|
Empty `{}` is reserved for blocks/lambdas; use `Map()` for an empty map.
|
||||||
|
|
||||||
To remove item from the collection. use `remove`. It returns last removed item or null. Be careful if you
|
To remove item from the collection. use `remove`. It returns last removed item or null. Be careful if you
|
||||||
hold nulls in the map - this is not a recommended practice when using `remove` returned value. `clear()`
|
hold nulls in the map - this is not a recommended practice when using `remove` returned value. `clear()`
|
||||||
removes all.
|
removes all.
|
||||||
@ -110,4 +143,36 @@ is equal.
|
|||||||
assert( m1 != m3 )
|
assert( m1 != m3 )
|
||||||
>>> void
|
>>> void
|
||||||
|
|
||||||
|
## Spreads and merging
|
||||||
|
|
||||||
|
Inside map literals you can spread another map with `...` and items will be merged left-to-right; rightmost wins:
|
||||||
|
|
||||||
|
val base = { a: 1, b: 2 }
|
||||||
|
val m = { a: 0, ...base, b: 3, c: 4 }
|
||||||
|
assertEquals(1, m["a"]) // base overwrites a:0
|
||||||
|
assertEquals(3, m["b"]) // literal overwrites spread
|
||||||
|
assertEquals(4, m["c"]) // new key
|
||||||
|
>>> void
|
||||||
|
|
||||||
|
Maps and entries can also be merged with `+` and `+=`:
|
||||||
|
|
||||||
|
val m1 = ("x" => 1) + ("y" => 2)
|
||||||
|
assertEquals(1, m1["x"])
|
||||||
|
assertEquals(2, m1["y"])
|
||||||
|
|
||||||
|
val m2 = { "a": 10 } + ("b" => 20)
|
||||||
|
assertEquals(10, m2["a"])
|
||||||
|
assertEquals(20, m2["b"])
|
||||||
|
|
||||||
|
var m3 = { a: 1 }
|
||||||
|
m3 += ("b" => 2)
|
||||||
|
assertEquals(1, m3["a"])
|
||||||
|
assertEquals(2, m3["b"])
|
||||||
|
>>> void
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
- Map literals always use string keys (identifier keys are converted to strings).
|
||||||
|
- Spreads inside map literals and `+`/`+=` merges require string keys on the right-hand side; this aligns with named-argument splats.
|
||||||
|
- When you need computed or non-string keys, use the constructor form `Map(...)` or build entries with `=>` and then merge.
|
||||||
|
|
||||||
[Collection](Collection.md)
|
[Collection](Collection.md)
|
||||||
@ -126,6 +126,14 @@ Splat (`...`) of a Map provides named arguments to the call. Only string keys ar
|
|||||||
assertEquals(["A?","B!","c","D!"], r)
|
assertEquals(["A?","B!","c","D!"], r)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
The same with a map literal is often more concise. Define the literal, then splat the variable:
|
||||||
|
|
||||||
|
fun test(a="a", b="b", c="c", d="d") { [a, b, c, d] }
|
||||||
|
val patch = { d: "D!", b: "B!" }
|
||||||
|
val r = test("A?", ...patch)
|
||||||
|
assertEquals(["A?","B!","c","D!"], r)
|
||||||
|
>>> void
|
||||||
|
|
||||||
Constraints:
|
Constraints:
|
||||||
|
|
||||||
- Map splat keys must be strings; otherwise, a clean error is thrown.
|
- Map splat keys must be strings; otherwise, a clean error is thrown.
|
||||||
|
|||||||
@ -1,82 +1,93 @@
|
|||||||
Note for AI. please ignore this file until this remark is removed.
|
|
||||||
|
|
||||||
# Map literals proposal
|
# Map literals — refined proposal
|
||||||
|
|
||||||
Note to the AI: please ignore this file until this note is removed.
|
Implement JavaScript-like literals for maps. The syntax and semantics align with named arguments in function calls, but map literals are expressions that construct `Map` values.
|
||||||
|
|
||||||
Implement javascript-like literals for maps. It uses the similar syntax already adopted for named arguments in function calls so it should be organic and familiar, as the purpose is similar.
|
Keys can be either:
|
||||||
|
- string literals: `{ "some key": value }`, or
|
||||||
|
- identifiers: `{ name: expr }`, where the key becomes the string `"name"`.
|
||||||
|
|
||||||
String literal keys
|
Identifier shorthand inside map literals is supported:
|
||||||
|
- `{ name: }` desugars to `{ "name": name }`.
|
||||||
|
|
||||||
|
Property access sugar is not provided for maps: use bracket access only, e.g. `m["a"]`, not `m.a`.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
|
||||||
```lyng
|
```lyng
|
||||||
val m = { a: "foo", b: "bar" }
|
val x = 2
|
||||||
assertEqual(m.a, "foo")
|
val m = { "a": 1, x: x*10, y: }
|
||||||
assertEqual(m.b, "bar")
|
assertEquals(1, m["a"]) // string-literal key
|
||||||
|
assertEquals(20, m["x"]) // identifier key
|
||||||
|
assertEquals(2, m["y"]) // identifier shorthand
|
||||||
```
|
```
|
||||||
|
|
||||||
The map literal is a map with keys that are string literals only. This is important. In a relatively rare case when keys are calculated, or extravagant but still possible case when keys are of different types, literal could be combined with "=>":
|
Spreads (splats) in map literals are allowed and merged left-to-right with “rightmost wins” semantics:
|
||||||
|
|
||||||
```lyng
|
```lyng
|
||||||
val k1 = "bar"
|
val base = { a: 1, b: 2 }
|
||||||
val m = { "foo": 123 } + k1 => "buzz"
|
val m = { a: 0, ...base, b: 3, c: 4 }
|
||||||
// this is same as Map("foo" => 123) + Map("bar" => k2) but can be optimized by compiler
|
assertEquals(1, m["a"]) // base overwrites a:0
|
||||||
assertEqual(m["foo"], 123)
|
assertEquals(3, m["b"]) // literal overwrites spread
|
||||||
assertEqual(m["bar"], "buzz")
|
assertEquals(4, m["c"]) // new key
|
||||||
```
|
```
|
||||||
|
|
||||||
The lambda syntax is different, it can start with the `map_lteral_start` above, it should produce compile time error, so we can add map literals of this sort.
|
Trailing commas are allowed (optional):
|
||||||
|
|
||||||
Also, we will allow splats in map literals:
|
```lyng
|
||||||
|
val m = {
|
||||||
```
|
"a": 1,
|
||||||
val m = { foo: "bar", ...{bar: "buzz"} }
|
b: 2,
|
||||||
assertEquals("bar",m["foo"])
|
...other,
|
||||||
assertEquals("buzz", m["bar"])
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
When the literal argument and splats are used together, they must be evaluated left-to-right with allowed overwriting
|
Duplicate keys among literal entries (including identifier shorthand) are a compile-time error:
|
||||||
between named elements and splats, allowing any combination and multiple splats:
|
|
||||||
|
|
||||||
```
|
```lyng
|
||||||
val m = { foo: "bar", ...{bar: "buzz"}, ...{foo: "foobar"}, bar: "bar" }
|
{ foo: 1, "foo": 2 } // error: duplicate key "foo"
|
||||||
assertEquals("foobar",m["foo"])
|
{ foo:, foo: 2 } // error: duplicate key "foo"
|
||||||
assertEquals("bar", m["bar"])
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Still we disallow duplicating _string literals_:
|
Spreads are evaluated at runtime. Overlaps from spreads are resolved by last write wins. If a spread is not a map, or yields a map with non-string keys, it’s a runtime error.
|
||||||
|
|
||||||
```
|
Merging with `+`/`+=` and entries:
|
||||||
// this is an compile-time exception:
|
|
||||||
{ foo: 1, bar: 2, foo: 3 }
|
```lyng
|
||||||
|
("1" => 10) + ("2" => 20) // Map("1"=>10, "2"=>20)
|
||||||
|
{ "1": 10 } + ("2" => 20) // same
|
||||||
|
{ "1": 10 } + { "2": 20 } // same
|
||||||
|
|
||||||
|
var m = { "a": 1 }
|
||||||
|
m += ("b" => 2) // m = { "a":1, "b":2 }
|
||||||
```
|
```
|
||||||
|
|
||||||
Special syntax allows to insert key-value pair from the variable which name should be the key, and content is value:
|
Rightmost wins on duplicates consistently across spreads and merges. All map merging operations require string keys; encountering a non-string key during merge is a runtime error.
|
||||||
|
|
||||||
|
Empty map literal `{}` is not supported to avoid ambiguity with blocks/lambdas. Use `Map()` for an empty map.
|
||||||
|
|
||||||
|
Lambda disambiguation
|
||||||
|
- A `{ ... }` with typed lambda parameters must have a top-level `->` in its header. The compiler disambiguates by looking for a top-level `->`. If none is found, it attempts to parse a map literal; if that fails, it is parsed as a lambda or block.
|
||||||
|
|
||||||
|
Grammar (EBNF)
|
||||||
|
|
||||||
```
|
```
|
||||||
val foo = "bar"
|
ws = zero or more whitespace (incl. newline/comments)
|
||||||
val bar = "buzz"
|
map_literal = '{' ws map_entries ws '}'
|
||||||
assertEquals( {foo: "bar", bar: "buzz"}, { *foo, *bar } )
|
map_entries = map_entry ( ws ',' ws map_entry )* ( ws ',' )?
|
||||||
|
map_entry = map_key ws ':' ws map_value_opt
|
||||||
|
| '...' ws expression
|
||||||
|
map_key = string_literal | ID
|
||||||
|
map_value_opt = expression | ε // ε allowed only if map_key is ID
|
||||||
```
|
```
|
||||||
|
|
||||||
Question to the AI: maybe better syntax than asterisk for that case?
|
Notes:
|
||||||
|
- Identifier shorthand (`id:`) is allowed only for identifiers, not string-literal keys.
|
||||||
|
- Spreads accept any expression; at runtime it must yield a `Map` with string keys.
|
||||||
|
- Duplicate keys are detected at compile time among literal keys; spreads are merged at runtime with last-wins.
|
||||||
|
|
||||||
So, summarizing, overwriting/duplication rules are:
|
Rationale
|
||||||
|
- The `{ name: value }` style is familiar and ergonomic.
|
||||||
- string literals can't duplicate
|
- Disambiguation with lambdas leverages the required `->` in typed lambda headers.
|
||||||
- splats add or update content, effectively overwrite preceding content,
|
- Avoiding `m.a` sidesteps method/field shadowing and keeps semantics clear.
|
||||||
- string literals overwrite content received from preceding splats (as no duplication string literal keys allowed)
|
|
||||||
- the priority and order is left-to-right, rightmost wins.
|
|
||||||
- var inclusion is treated as form of the literal
|
|
||||||
|
|
||||||
This approach resolves the ambiguity from lambda syntax, as
|
|
||||||
|
|
||||||
```ebnf
|
|
||||||
ws = zero or more whitespace characters including newline
|
|
||||||
map_literal start = "{", ws, (s1 | s2 | s3)
|
|
||||||
s1 = string_literal, ws, ":", ws, expression
|
|
||||||
s2 = "...", string_literal
|
|
||||||
s3 = "*", string_literal
|
|
||||||
```
|
|
||||||
|
|
||||||
as we can see, `map_literal_start` is not a valid lambda beginning so it is not create ambiguity.
|
|
||||||
|
|
||||||
|
|||||||
@ -676,14 +676,63 @@ Please see [Set] for detailed description.
|
|||||||
|
|
||||||
# Maps
|
# Maps
|
||||||
|
|
||||||
Maps are unordered collection of key-value pairs, where keys are unique. See [Map] for details. Map also
|
Maps are unordered collections of key-value pairs, where keys are unique. Maps are also [Iterable].
|
||||||
are [Iterable]:
|
|
||||||
|
|
||||||
val m = Map( "foo" => 77, "bar" => "buzz" )
|
You can create them either with the classic constructor (still supported):
|
||||||
assertEquals( m["foo"], 77 )
|
|
||||||
|
val old = Map( "foo" => 77, "bar" => "buzz" )
|
||||||
|
assertEquals( old["foo"], 77 )
|
||||||
>>> void
|
>>> void
|
||||||
|
|
||||||
Please see [Map] reference for detailed description on using Maps.
|
…or with map literals, which are often more convenient:
|
||||||
|
|
||||||
|
val x = 10
|
||||||
|
val y = 10
|
||||||
|
val m = { "a": 1, x: x * 2, y: }
|
||||||
|
// identifier keys become strings; `y:` is shorthand for y: y
|
||||||
|
assertEquals(1, m["a"]) // string-literal key
|
||||||
|
assertEquals(20, m["x"]) // identifier key
|
||||||
|
assertEquals(10, m["y"]) // shorthand
|
||||||
|
>>> void
|
||||||
|
|
||||||
|
Map literals support trailing commas for nicer diffs:
|
||||||
|
|
||||||
|
val m2 = {
|
||||||
|
"a": 1,
|
||||||
|
b: 2,
|
||||||
|
}
|
||||||
|
assertEquals(1, m2["a"])
|
||||||
|
assertEquals(2, m2["b"])
|
||||||
|
>>> void
|
||||||
|
|
||||||
|
You can spread other maps inside a literal with `...`. Items merge left-to-right and the rightmost value wins:
|
||||||
|
|
||||||
|
val base = { a: 1, b: 2 }
|
||||||
|
val merged = { a: 0, ...base, b: 3, c: 4 }
|
||||||
|
assertEquals(1, merged["a"]) // base overwrites a:0
|
||||||
|
assertEquals(3, merged["b"]) // literal overwrites spread
|
||||||
|
assertEquals(4, merged["c"]) // new key
|
||||||
|
>>> void
|
||||||
|
|
||||||
|
Merging also works with `+` and `+=`, and you can combine maps and entries conveniently:
|
||||||
|
|
||||||
|
val m3 = { "a": 10 } + ("b" => 20)
|
||||||
|
assertEquals(10, m3["a"])
|
||||||
|
assertEquals(20, m3["b"])
|
||||||
|
|
||||||
|
var m4 = ("x" => 1) + ("y" => 2) // entry + entry → Map
|
||||||
|
m4 += { z: 3 } // merge literal
|
||||||
|
assertEquals(1, m4["x"])
|
||||||
|
assertEquals(2, m4["y"])
|
||||||
|
assertEquals(3, m4["z"])
|
||||||
|
>>> void
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
- Access keys with brackets: `m["key"]`. There is no `m.key` sugar.
|
||||||
|
- Empty `{}` remains a block/lambda; use `Map()` to create an empty map.
|
||||||
|
- When you need computed (expression) keys or non-string keys, use `Map(...)` constructor with entries, e.g. `Map( ("a" + "b") => 1 )`, then merge with a literal if needed: `{ base: } + (computedKey => 42)`.
|
||||||
|
|
||||||
|
Please see the [Map] reference for a deeper guide.
|
||||||
|
|
||||||
# Flow control operators
|
# Flow control operators
|
||||||
|
|
||||||
|
|||||||
@ -13,6 +13,7 @@
|
|||||||
{ "include": "#keywords" },
|
{ "include": "#keywords" },
|
||||||
{ "include": "#constants" },
|
{ "include": "#constants" },
|
||||||
{ "include": "#types" },
|
{ "include": "#types" },
|
||||||
|
{ "include": "#mapLiterals" },
|
||||||
{ "include": "#namedArgs" },
|
{ "include": "#namedArgs" },
|
||||||
{ "include": "#annotations" },
|
{ "include": "#annotations" },
|
||||||
{ "include": "#labels" },
|
{ "include": "#labels" },
|
||||||
@ -42,6 +43,25 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"annotations": { "patterns": [ { "name": "entity.name.label.at.lyng", "match": "@[\\p{L}_][\\p{L}\\p{N}_]*:" }, { "name": "storage.modifier.annotation.lyng", "match": "@[\\p{L}_][\\p{L}\\p{N}_]*" } ] },
|
"annotations": { "patterns": [ { "name": "entity.name.label.at.lyng", "match": "@[\\p{L}_][\\p{L}\\p{N}_]*:" }, { "name": "storage.modifier.annotation.lyng", "match": "@[\\p{L}_][\\p{L}\\p{N}_]*" } ] },
|
||||||
|
"mapLiterals": {
|
||||||
|
"patterns": [
|
||||||
|
{
|
||||||
|
"name": "meta.map.entry.lyng",
|
||||||
|
"match": "(?:(?<=\\{|,))\\s*(\"[^\"]*\"|[\\p{L}_][\\p{L}\\p{N}_]*)\\s*(:)(?!:)",
|
||||||
|
"captures": {
|
||||||
|
"1": { "name": "variable.other.property.key.lyng" },
|
||||||
|
"2": { "name": "punctuation.separator.colon.lyng" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "meta.map.spread.lyng",
|
||||||
|
"match": "(?:(?<=\\{|,))\\s*(\\.\\.\\.)",
|
||||||
|
"captures": {
|
||||||
|
"1": { "name": "keyword.operator.spread.lyng" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
"namedArgs": {
|
"namedArgs": {
|
||||||
"patterns": [
|
"patterns": [
|
||||||
{
|
{
|
||||||
|
|||||||
@ -21,7 +21,7 @@ import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl
|
|||||||
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
||||||
|
|
||||||
group = "net.sergeych"
|
group = "net.sergeych"
|
||||||
version = "1.0.1-SNAPSHOT"
|
version = "1.0.3"
|
||||||
|
|
||||||
// Removed legacy buildscript classpath declarations; plugins are applied via the plugins DSL below
|
// Removed legacy buildscript classpath declarations; plugins are applied via the plugins DSL below
|
||||||
|
|
||||||
|
|||||||
@ -572,7 +572,15 @@ class Compiler(
|
|||||||
blockArgument = true,
|
blockArgument = true,
|
||||||
isOptional = t.type == Token.Type.NULL_COALESCE_BLOCKINVOKE
|
isOptional = t.type == Token.Type.NULL_COALESCE_BLOCKINVOKE
|
||||||
)
|
)
|
||||||
} ?: parseLambdaExpression()
|
} ?: run {
|
||||||
|
// Disambiguate between lambda and map literal.
|
||||||
|
// Heuristic: if there is a top-level '->' before the closing '}', it's a lambda.
|
||||||
|
// Otherwise, try to parse a map literal; if it fails, fall back to lambda.
|
||||||
|
val isLambda = hasTopLevelArrowBeforeRbrace()
|
||||||
|
if (!isLambda) {
|
||||||
|
parseMapLiteralOrNull() ?: parseLambdaExpression()
|
||||||
|
} else parseLambdaExpression()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Token.Type.RBRACKET, Token.Type.RPAREN -> {
|
Token.Type.RBRACKET, Token.Type.RPAREN -> {
|
||||||
@ -674,6 +682,115 @@ class Compiler(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Look ahead from current position (right after a leading '{') to find a top-level '->' before the matching '}'.
|
||||||
|
* Returns true if such arrow is found, meaning the construct should be parsed as a lambda.
|
||||||
|
* The scanner respects nested braces depth; only depth==1 arrows count.
|
||||||
|
* The current cursor is restored on exit.
|
||||||
|
*/
|
||||||
|
private fun hasTopLevelArrowBeforeRbrace(): Boolean {
|
||||||
|
val start = cc.savePos()
|
||||||
|
var depth = 1
|
||||||
|
var found = false
|
||||||
|
while (cc.hasNext()) {
|
||||||
|
val t = cc.next()
|
||||||
|
when (t.type) {
|
||||||
|
Token.Type.LBRACE -> depth++
|
||||||
|
Token.Type.RBRACE -> {
|
||||||
|
depth--
|
||||||
|
if (depth == 0) break
|
||||||
|
}
|
||||||
|
Token.Type.ARROW -> if (depth == 1) {
|
||||||
|
found = true
|
||||||
|
// Do not break; we still restore position below
|
||||||
|
}
|
||||||
|
else -> {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cc.restorePos(start)
|
||||||
|
return found
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempt to parse a map literal starting at the position after '{'.
|
||||||
|
* Returns null if the sequence does not look like a map literal (e.g., empty or first token is not STRING/ID/ELLIPSIS),
|
||||||
|
* in which case caller should treat it as a lambda/block.
|
||||||
|
* When it recognizes a map literal, it commits and throws on syntax errors.
|
||||||
|
*/
|
||||||
|
private suspend fun parseMapLiteralOrNull(): ObjRef? {
|
||||||
|
val startAfterLbrace = cc.savePos()
|
||||||
|
// Peek first non-ws token to decide whether it's likely a map literal
|
||||||
|
val first = cc.peekNextNonWhitespace()
|
||||||
|
// Empty {} should NOT be taken as a map literal to preserve block/lambda semantics
|
||||||
|
if (first.type == Token.Type.RBRACE) return null
|
||||||
|
if (first.type !in listOf(Token.Type.STRING, Token.Type.ID, Token.Type.ELLIPSIS)) return null
|
||||||
|
|
||||||
|
// Commit to map literal parsing
|
||||||
|
cc.skipWsTokens()
|
||||||
|
val entries = mutableListOf<net.sergeych.lyng.obj.MapLiteralEntry>()
|
||||||
|
val usedKeys = mutableSetOf<String>()
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
// Skip whitespace/comments/newlines between entries
|
||||||
|
val t0 = cc.nextNonWhitespace()
|
||||||
|
when (t0.type) {
|
||||||
|
Token.Type.RBRACE -> {
|
||||||
|
// end of map literal
|
||||||
|
return net.sergeych.lyng.obj.MapLiteralRef(entries)
|
||||||
|
}
|
||||||
|
Token.Type.COMMA -> {
|
||||||
|
// allow stray commas; continue
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
Token.Type.ELLIPSIS -> {
|
||||||
|
// spread element: ... expression
|
||||||
|
val expr = parseExpressionLevel() ?: throw ScriptError(t0.pos, "invalid map spread: expecting expression")
|
||||||
|
entries += net.sergeych.lyng.obj.MapLiteralEntry.Spread(expr)
|
||||||
|
// Expect comma or '}' next; loop will handle
|
||||||
|
}
|
||||||
|
Token.Type.STRING, Token.Type.ID -> {
|
||||||
|
val isIdKey = t0.type == Token.Type.ID
|
||||||
|
val keyName = if (isIdKey) t0.value else t0.value
|
||||||
|
// After key we require ':'
|
||||||
|
cc.skipWsTokens()
|
||||||
|
val colon = cc.next()
|
||||||
|
if (colon.type != Token.Type.COLON) {
|
||||||
|
// Not a map literal after all; backtrack and signal null
|
||||||
|
cc.restorePos(startAfterLbrace)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
// Check for shorthand (only for id-keys): if next non-ws is ',' or '}'
|
||||||
|
cc.skipWsTokens()
|
||||||
|
val next = cc.next()
|
||||||
|
if ((next.type == Token.Type.COMMA || next.type == Token.Type.RBRACE)) {
|
||||||
|
if (!isIdKey) throw ScriptError(next.pos, "missing value after string-literal key '$keyName'")
|
||||||
|
// id: shorthand; value is the variable with the same name
|
||||||
|
// rewind one step if RBRACE so outer loop can handle it
|
||||||
|
if (next.type == Token.Type.RBRACE) cc.previous()
|
||||||
|
// Duplicate detection for literals only
|
||||||
|
if (!usedKeys.add(keyName)) throw ScriptError(t0.pos, "duplicate key '$keyName'")
|
||||||
|
entries += net.sergeych.lyng.obj.MapLiteralEntry.Named(keyName, net.sergeych.lyng.obj.LocalVarRef(keyName, t0.pos))
|
||||||
|
// If the token was COMMA, the loop continues; if it's RBRACE, next iteration will end
|
||||||
|
} else {
|
||||||
|
// There is a value expression: push back token and parse expression
|
||||||
|
cc.previous()
|
||||||
|
val valueRef = parseExpressionLevel() ?: throw ScriptError(colon.pos, "expecting map entry value")
|
||||||
|
if (!usedKeys.add(keyName)) throw ScriptError(t0.pos, "duplicate key '$keyName'")
|
||||||
|
entries += net.sergeych.lyng.obj.MapLiteralEntry.Named(keyName, valueRef)
|
||||||
|
// After value, allow optional comma; do not require it
|
||||||
|
cc.skipTokenOfType(Token.Type.COMMA, isOptional = true)
|
||||||
|
// The loop will continue and eventually see '}'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
// Not a map literal; backtrack and let caller treat as lambda
|
||||||
|
cc.restorePos(startAfterLbrace)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parse argument declaration, used in lambda (and later in fn too)
|
* Parse argument declaration, used in lambda (and later in fn too)
|
||||||
* @return declaration or null if there is no valid list of arguments
|
* @return declaration or null if there is no valid list of arguments
|
||||||
|
|||||||
@ -75,6 +75,12 @@ class ObjMapEntry(val key: Obj, val value: Obj) : Obj() {
|
|||||||
addFn("size") { 2.toObj() }
|
addFn("size") { 2.toObj() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun plus(scope: Scope, other: Obj): Obj {
|
||||||
|
// Build a new map starting from this entry, then merge `other`.
|
||||||
|
val result = ObjMap(mutableMapOf(key to value))
|
||||||
|
return result.plus(scope, other)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class ObjMap(val map: MutableMap<Obj, Obj> = mutableMapOf()) : Obj() {
|
class ObjMap(val map: MutableMap<Obj, Obj> = mutableMapOf()) : Obj() {
|
||||||
@ -96,7 +102,22 @@ class ObjMap(val map: MutableMap<Obj, Obj> = mutableMapOf()) : Obj() {
|
|||||||
if( other is ObjMap && other.map == map) return 0
|
if( other is ObjMap && other.map == map) return 0
|
||||||
return -1
|
return -1
|
||||||
}
|
}
|
||||||
override fun toString(): String = map.toString()
|
|
||||||
|
override suspend fun toString(scope: Scope, calledFromLyng: Boolean): ObjString {
|
||||||
|
val reusult = buildString {
|
||||||
|
append("Map(")
|
||||||
|
var first = true
|
||||||
|
for( (k,v) in map) {
|
||||||
|
if( !first ) append(",")
|
||||||
|
append(k.inspect(scope))
|
||||||
|
append(" => ")
|
||||||
|
append(v.toString(scope).value)
|
||||||
|
first = false
|
||||||
|
}
|
||||||
|
append(")")
|
||||||
|
}
|
||||||
|
return ObjString(reusult)
|
||||||
|
}
|
||||||
|
|
||||||
override fun hashCode(): Int {
|
override fun hashCode(): Int {
|
||||||
return map.hashCode()
|
return map.hashCode()
|
||||||
@ -187,4 +208,44 @@ class ObjMap(val map: MutableMap<Obj, Obj> = mutableMapOf()) : Obj() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Merge operations
|
||||||
|
override suspend fun plus(scope: Scope, other: Obj): Obj {
|
||||||
|
val result = ObjMap(map.toMutableMap())
|
||||||
|
result.mergeIn(scope, other)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun plusAssign(scope: Scope, other: Obj): Obj {
|
||||||
|
mergeIn(scope, other)
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun mergeIn(scope: Scope, other: Obj) {
|
||||||
|
when (other) {
|
||||||
|
is ObjMap -> {
|
||||||
|
// Rightmost wins: copy all entries from `other` over existing ones
|
||||||
|
for ((k, v) in other.map) {
|
||||||
|
val key = k as? ObjString ?: scope.raiseIllegalArgument("map merge expects string keys; got $k")
|
||||||
|
map[key] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is ObjMapEntry -> {
|
||||||
|
val key = other.key as? ObjString ?: scope.raiseIllegalArgument("map merge expects string keys; got ${other.key}")
|
||||||
|
map[key] = other.value
|
||||||
|
}
|
||||||
|
is ObjList -> {
|
||||||
|
// Treat as list of map entries
|
||||||
|
for (e in other.list) {
|
||||||
|
val entry = when (e) {
|
||||||
|
is ObjMapEntry -> e
|
||||||
|
else -> scope.raiseIllegalArgument("map can only be merged with MapEntry elements; got $e")
|
||||||
|
}
|
||||||
|
val key = entry.key as? ObjString ?: scope.raiseIllegalArgument("map merge expects string keys; got ${entry.key}")
|
||||||
|
map[key] = entry.value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else -> scope.raiseIllegalArgument("map can only be merged with Map, MapEntry, or List<MapEntry>")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -1492,6 +1492,37 @@ class ListLiteralRef(private val entries: List<ListEntry>) : ObjRef {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Map literal support ---
|
||||||
|
|
||||||
|
sealed class MapLiteralEntry {
|
||||||
|
data class Named(val key: String, val value: ObjRef) : MapLiteralEntry()
|
||||||
|
data class Spread(val ref: ObjRef) : MapLiteralEntry()
|
||||||
|
}
|
||||||
|
|
||||||
|
class MapLiteralRef(private val entries: List<MapLiteralEntry>) : ObjRef {
|
||||||
|
override suspend fun get(scope: Scope): ObjRecord {
|
||||||
|
val result = ObjMap(mutableMapOf())
|
||||||
|
for (e in entries) {
|
||||||
|
when (e) {
|
||||||
|
is MapLiteralEntry.Named -> {
|
||||||
|
val v = if (PerfFlags.RVAL_FASTPATH) e.value.evalValue(scope) else e.value.get(scope).value
|
||||||
|
result.map[ObjString(e.key)] = v
|
||||||
|
}
|
||||||
|
is MapLiteralEntry.Spread -> {
|
||||||
|
val m = if (PerfFlags.RVAL_FASTPATH) e.ref.evalValue(scope) else e.ref.get(scope).value
|
||||||
|
if (m !is ObjMap) scope.raiseIllegalArgument("spread element in map literal must be a Map")
|
||||||
|
// Enforce string keys for map literals
|
||||||
|
for ((k, v) in m.map) {
|
||||||
|
val sKey = k as? ObjString ?: scope.raiseIllegalArgument("spread map must have string keys; got $k")
|
||||||
|
result.map[sKey] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result.asReadonly
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Range literal: left .. right or left ..< right. Right may be omitted in certain contexts.
|
* Range literal: left .. right or left ..< right. Right may be omitted in certain contexts.
|
||||||
*/
|
*/
|
||||||
|
|||||||
144
lynglib/src/commonTest/kotlin/MapLiteralTest.kt
Normal file
144
lynglib/src/commonTest/kotlin/MapLiteralTest.kt
Normal file
@ -0,0 +1,144 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2025 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.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Map literal and merging tests
|
||||||
|
*/
|
||||||
|
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import net.sergeych.lyng.ExecutionError
|
||||||
|
import net.sergeych.lyng.ScriptError
|
||||||
|
import net.sergeych.lyng.eval
|
||||||
|
import kotlin.test.Test
|
||||||
|
import kotlin.test.assertEquals
|
||||||
|
import kotlin.test.assertFailsWith
|
||||||
|
|
||||||
|
class MapLiteralTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun basicStringAndIdKeysAndShorthand() = runTest {
|
||||||
|
eval(
|
||||||
|
"""
|
||||||
|
val x = 2
|
||||||
|
val y = 2
|
||||||
|
val m = { "a": 1, x: x*10, y: }
|
||||||
|
assertEquals(1, m["a"])
|
||||||
|
assertEquals(20, m["x"])
|
||||||
|
assertEquals(2, m["y"])
|
||||||
|
""".trimIndent()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun spreadsAndOverwriteOrder() = runTest {
|
||||||
|
eval(
|
||||||
|
"""
|
||||||
|
val base = { a: 1, b: 2 }
|
||||||
|
val m = { a: 0, ...base, b: 3, c: 4 }
|
||||||
|
assertEquals(1, m["a"]) // base overwrites a:0
|
||||||
|
assertEquals(3, m["b"]) // literal overwrites spread
|
||||||
|
assertEquals(4, m["c"]) // new key
|
||||||
|
""".trimIndent()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun trailingCommaAccepted() = runTest {
|
||||||
|
eval(
|
||||||
|
"""
|
||||||
|
val m = { "a": 1, b: 2, }
|
||||||
|
assertEquals(1, m["a"])
|
||||||
|
assertEquals(2, m["b"])
|
||||||
|
""".trimIndent()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun duplicateLiteralKeysAreCompileTimeError() = runTest {
|
||||||
|
assertFailsWith<ScriptError> {
|
||||||
|
eval("""{ foo: 1, " + '"' + "foo" + '"' + ": 2 }""".trimIndent())
|
||||||
|
}
|
||||||
|
assertFailsWith<ScriptError> {
|
||||||
|
eval("""{ foo:, foo: 2 }""".trimIndent())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun lambdaDisambiguationWithTypedArgs() = runTest {
|
||||||
|
eval(
|
||||||
|
"""
|
||||||
|
val f = { x: Int, y: Int -> x + y }
|
||||||
|
assertEquals(3, f(1,2))
|
||||||
|
""".trimIndent()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun plusMergingAndPlusAssign() = runTest {
|
||||||
|
eval(
|
||||||
|
"""
|
||||||
|
val m1 = ("1" => 10) + ("2" => 20)
|
||||||
|
assertEquals(10, m1["1"])
|
||||||
|
assertEquals(20, m1["2"])
|
||||||
|
|
||||||
|
val m2 = { "1": 10 } + ("2" => 20)
|
||||||
|
assertEquals(10, m2["1"])
|
||||||
|
assertEquals(20, m2["2"])
|
||||||
|
|
||||||
|
val m3 = { "1": 10 } + { "2": 20 }
|
||||||
|
assertEquals(10, m3["1"])
|
||||||
|
assertEquals(20, m3["2"])
|
||||||
|
|
||||||
|
var m = { "a": 1 }
|
||||||
|
m += ("b" => 2)
|
||||||
|
assertEquals(1, m["a"])
|
||||||
|
assertEquals(2, m["b"])
|
||||||
|
""".trimIndent()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun spreadNonMapIsRuntimeError() = runTest {
|
||||||
|
assertFailsWith<ExecutionError> {
|
||||||
|
eval("""{ ...[1,2,3] }""")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun spreadNonStringKeysIsRuntimeError() = runTest {
|
||||||
|
assertFailsWith<ExecutionError> {
|
||||||
|
eval("""{ ...Map(1 => "x") }""")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun mergeNonStringKeyIsRuntimeError() = runTest {
|
||||||
|
assertFailsWith<ExecutionError> {
|
||||||
|
eval("""
|
||||||
|
val e = (1 => "x")
|
||||||
|
{ "a": 1 } + e
|
||||||
|
""")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun shorthandUndefinedIdIsError() = runTest {
|
||||||
|
assertFailsWith<ExecutionError> {
|
||||||
|
eval("""{ z: }""")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,57 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2025 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.highlight
|
||||||
|
|
||||||
|
import kotlin.test.Test
|
||||||
|
import kotlin.test.assertTrue
|
||||||
|
|
||||||
|
class MapLiteralHighlightTest {
|
||||||
|
|
||||||
|
private fun spansToLabeled(text: String, spans: List<HighlightSpan>): List<Pair<String, HighlightKind>> =
|
||||||
|
spans.map { text.substring(it.range.start, it.range.endExclusive) to it.kind }
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun highlightsMapLiteralBasics() {
|
||||||
|
val text = """
|
||||||
|
val x = 2
|
||||||
|
val m = { "a": 1, b: , ...base, }
|
||||||
|
""".trimIndent()
|
||||||
|
|
||||||
|
val spans = SimpleLyngHighlighter().highlight(text)
|
||||||
|
val labeled = spansToLabeled(text, spans)
|
||||||
|
|
||||||
|
// Brace punctuation
|
||||||
|
assertTrue(labeled.any { it.first == "{" && it.second == HighlightKind.Punctuation })
|
||||||
|
assertTrue(labeled.any { it.first == "}" && it.second == HighlightKind.Punctuation })
|
||||||
|
|
||||||
|
// String key and number value
|
||||||
|
assertTrue(labeled.any { it.first == "\"a\"" && it.second == HighlightKind.String })
|
||||||
|
assertTrue(labeled.any { it.first == "1" && it.second == HighlightKind.Number })
|
||||||
|
|
||||||
|
// Identifier key and shorthand (b:)
|
||||||
|
assertTrue(labeled.any { it.first == "b" && it.second == HighlightKind.Identifier })
|
||||||
|
// The colon after key is punctuation
|
||||||
|
assertTrue(labeled.any { it.first == ":" && it.second == HighlightKind.Punctuation })
|
||||||
|
|
||||||
|
// Spread operator
|
||||||
|
assertTrue(labeled.any { it.first == "..." && it.second == HighlightKind.Operator })
|
||||||
|
|
||||||
|
// Trailing comma
|
||||||
|
assertTrue(labeled.any { it.first == "," && it.second == HighlightKind.Punctuation })
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -56,7 +56,7 @@ kotlin {
|
|||||||
implementation("org.jetbrains.compose.runtime:runtime:1.9.3")
|
implementation("org.jetbrains.compose.runtime:runtime:1.9.3")
|
||||||
implementation("org.jetbrains.compose.html:html-core:1.9.3")
|
implementation("org.jetbrains.compose.html:html-core:1.9.3")
|
||||||
implementation(libs.kotlinx.coroutines.core)
|
implementation(libs.kotlinx.coroutines.core)
|
||||||
implementation(project(":lynglib"))
|
api(project(":lynglib"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val jsTest by getting {
|
val jsTest by getting {
|
||||||
|
|||||||
@ -80,12 +80,19 @@ import lyng.stdlib
|
|||||||
val data = 1..5 // or [1,2,3,4,5]
|
val data = 1..5 // or [1,2,3,4,5]
|
||||||
val evens2 = data.filter { it % 2 == 0 }.map { it * it }
|
val evens2 = data.filter { it % 2 == 0 }.map { it * it }
|
||||||
assertEquals([4, 16], evens2)
|
assertEquals([4, 16], evens2)
|
||||||
|
|
||||||
|
// Map literal with identifier keys, shorthand, and spread
|
||||||
|
val base = { a: 1, b: 2 }
|
||||||
|
val patch = { b: 3, c: }
|
||||||
|
val m = { "a": 0, ...base, ...patch, d: 4 }
|
||||||
|
assertEquals(1, m["a"]) // base overwrites 0
|
||||||
|
assertEquals(3, m["b"]) // patch overwrites base
|
||||||
|
assertEquals(4, m["d"]) // literal key
|
||||||
>>> void
|
>>> void
|
||||||
""".trimIndent()
|
""".trimIndent()
|
||||||
|
val mapHtml = "<pre><code>" + htmlEscape(code) + "</code></pre>"
|
||||||
val codeHtml = "<pre><code>" + htmlEscape(code) + "</code></pre>"
|
Div({ classes("markdown-body", "mt-3") }) {
|
||||||
Div({ classes("markdown-body") }) {
|
UnsafeRawHtml(highlightLyngHtml(ensureBootstrapCodeBlocks(mapHtml)))
|
||||||
UnsafeRawHtml(highlightLyngHtml(ensureBootstrapCodeBlocks(codeHtml)))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Short features list
|
// Short features list
|
||||||
|
|||||||
@ -17,6 +17,7 @@
|
|||||||
|
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import net.sergeych.lyng.LyngVersion
|
||||||
import net.sergeych.lyng.Scope
|
import net.sergeych.lyng.Scope
|
||||||
import net.sergeych.lyng.ScriptError
|
import net.sergeych.lyng.ScriptError
|
||||||
import net.sergeych.lyngweb.EditorWithOverlay
|
import net.sergeych.lyngweb.EditorWithOverlay
|
||||||
@ -134,7 +135,7 @@ fun TryLyngPage() {
|
|||||||
|
|
||||||
// Editor
|
// Editor
|
||||||
Div({ classes("mb-3") }) {
|
Div({ classes("mb-3") }) {
|
||||||
Div({ classes("form-label", "fw-semibold") }) { Text("Code") }
|
Div({ classes("form-label", "fw-semibold") }) { Text("Code (v${LyngVersion})") }
|
||||||
EditorWithOverlay(
|
EditorWithOverlay(
|
||||||
code = code,
|
code = code,
|
||||||
setCode = { code = it },
|
setCode = { code = it },
|
||||||
|
|||||||
@ -181,7 +181,7 @@
|
|||||||
<!-- Top-left version ribbon -->
|
<!-- Top-left version ribbon -->
|
||||||
<div class="corner-ribbon bg-danger text-white">
|
<div class="corner-ribbon bg-danger text-white">
|
||||||
<span class="me-3">
|
<span class="me-3">
|
||||||
v1.0.2
|
v1.0.3
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<!-- Fixed top navbar for the whole site -->
|
<!-- Fixed top navbar for the whole site -->
|
||||||
|
|||||||
@ -44,6 +44,26 @@ class HighlightSmokeTest {
|
|||||||
assertTrue(labeled.any { it.first == "2" && it.second == HighlightKind.Number })
|
assertTrue(labeled.any { it.first == "2" && it.second == HighlightKind.Number })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun highlightMapLiteralHtml() {
|
||||||
|
val text = """
|
||||||
|
val base = { a: 1, b: }
|
||||||
|
val m = { "a": 1, b: , ...base, }
|
||||||
|
""".trimIndent()
|
||||||
|
val html = SiteHighlight.renderHtml(text)
|
||||||
|
// Curly braces and commas are punctuation
|
||||||
|
assertContains(html, "<span class=\"hl-punc\">{</span>")
|
||||||
|
assertContains(html, "<span class=\"hl-punc\">}</span>")
|
||||||
|
assertTrue(html.contains("hl-punc") && html.contains(","), "comma should be punctuation")
|
||||||
|
// Colon after key is punctuation
|
||||||
|
assertContains(html, "<span class=\"hl-punc\">:</span>")
|
||||||
|
// Spread operator present
|
||||||
|
assertContains(html, "<span class=\"hl-op\">...</span>")
|
||||||
|
// String key and identifier key appear
|
||||||
|
assertContains(html, "<span class=\"hl-str\">\"a\"</span>")
|
||||||
|
assertContains(html, "<span class=\"hl-id\">b</span>")
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun renderHtmlContainsCorrectClasses() {
|
fun renderHtmlContainsCorrectClasses() {
|
||||||
val text = "assertEquals( [9,10], r.takeLast(2).toList() )"
|
val text = "assertEquals( [9,10], r.takeLast(2).toList() )"
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user