site: fixed .md/lyng code display

This commit is contained in:
Sergey Chernov 2026-04-30 09:50:10 +03:00
parent 53a9d21a19
commit 31fac1a73c
4 changed files with 82 additions and 5 deletions

View File

@ -63,7 +63,7 @@ server.get("/") {
} }
body { body {
h3 { +"Service is running" } h3 { +"Service is running" }
p { +("Path: ${request.path}" } p { +"Path: ${request.path}" }
} }
} }
} }

View File

@ -117,6 +117,28 @@ private fun mergeAdjacent(spans: List<HighlightSpan>): List<HighlightSpan> {
return out return out
} }
/**
* The parser expands interpolated strings into expression-like token streams whose
* synthetic tokens share source positions with the original string. Keep the
* widest source span at each offset and drop nested overlaps so renderers can
* rely on the public non-overlapping span contract.
*/
private fun removeOverlappingSpans(spans: List<HighlightSpan>): List<HighlightSpan> {
if (spans.size < 2) return spans
val sorted = spans.sortedWith(
compareBy<HighlightSpan> { it.range.start }
.thenByDescending { it.range.endExclusive - it.range.start }
)
val out = ArrayList<HighlightSpan>(sorted.size)
var coveredUntil = -1
for (span in sorted) {
if (span.range.start < coveredUntil) continue
out += span
coveredUntil = span.range.endExclusive
}
return out
}
/** Simple highlighter using the existing Lyng lexer (no incremental support yet). */ /** Simple highlighter using the existing Lyng lexer (no incremental support yet). */
class SimpleLyngHighlighter : LyngHighlighter { class SimpleLyngHighlighter : LyngHighlighter {
override fun highlight(text: String): List<HighlightSpan> { override fun highlight(text: String): List<HighlightSpan> {
@ -170,8 +192,8 @@ class SimpleLyngHighlighter : LyngHighlighter {
val overridden = applyEnumConstantHeuristics(text, src, tokens, raw) val overridden = applyEnumConstantHeuristics(text, src, tokens, raw)
// Adjust single-line comment spans to extend till EOL to compensate for lexer offset/length quirks // Adjust single-line comment spans to extend till EOL to compensate for lexer offset/length quirks
val adjusted = extendSingleLineCommentsToEol(text, overridden) val adjusted = extendSingleLineCommentsToEol(text, overridden)
// Spans are in order; merge adjacent of the same kind for compactness // Normalize spans, then merge adjacent spans of the same kind for compactness.
return mergeAdjacent(adjusted) return mergeAdjacent(removeOverlappingSpans(adjusted))
} }
} }

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2025 Sergey S. Chernov real.sergeych@gmail.com * Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -18,6 +18,7 @@
package net.sergeych.lyng.highlight package net.sergeych.lyng.highlight
import kotlin.test.Test import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertTrue import kotlin.test.assertTrue
class HighlightMappingTest { class HighlightMappingTest {
@ -72,6 +73,38 @@ class HighlightMappingTest {
assertTrue(labeled.any { it.first == "\"s\"" && it.second == HighlightKind.String }) assertTrue(labeled.any { it.first == "\"s\"" && it.second == HighlightKind.String })
} }
@Test
fun interpolatedStringSpansDoNotOverlap() {
val text = """p { +"Path: ${'$'}{request.path}" }"""
val spans = SimpleLyngHighlighter().highlight(text)
spans.zipWithNext().forEach { (a, b) ->
assertTrue(
a.range.endExclusive <= b.range.start,
"Highlight spans must not overlap: $a then $b in $spans"
)
}
assertTrue(
spansToLabeled(text, spans).any {
it.first == "\"Path: ${'$'}{request.path}\"" && it.second == HighlightKind.String
}
)
}
@Test
fun interpolatedStringRenderingDoesNotDuplicateText() {
val text = """p { +"Path: ${'$'}{request.path}" }"""
val rendered = buildString {
var pos = 0
for (span in SimpleLyngHighlighter().highlight(text)) {
if (span.range.start > pos) append(text.substring(pos, span.range.start))
append(text.substring(span.range.start, span.range.endExclusive))
pos = span.range.endExclusive
}
if (pos < text.length) append(text.substring(pos))
}
assertEquals(text, rendered)
}
@Test @Test
fun commentsHighlighted() { fun commentsHighlighted() {
val text = "// line\n/* block */" val text = "// line\n/* block */"

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2025 Sergey S. Chernov real.sergeych@gmail.com * Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -16,6 +16,7 @@
*/ */
import kotlin.test.Test import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFalse import kotlin.test.assertFalse
import kotlin.test.assertTrue import kotlin.test.assertTrue
@ -76,4 +77,25 @@ class LyngHighlightTest {
// the '<' should be escaped in HTML // the '<' should be escaped in HTML
assertTrue(html.contains("&lt;"), "Expected escaped < inside highlighted HTML: $html") assertTrue(html.contains("&lt;"), "Expected escaped < inside highlighted HTML: $html")
} }
@Test
fun rendersInterpolatedStringOnce() {
val md = """
```lyng
p { +"Path: ${'$'}{request.path}" }
```
""".trimIndent()
val html = renderMarkdown(md)
assertEquals(
1,
Regex("Path:").findAll(html).count(),
"Rendered markdown duplicated string content: $html"
)
assertEquals(
1,
Regex("request\\.path").findAll(html).count(),
"Rendered markdown duplicated interpolation content: $html"
)
}
} }