` has neither, and
+ * `

` has the colon but also a space.
+ */
+ private fun autolink(start: Int, end: Int): Int {
+ var at = start + 1
+ var addressed = false
+ while (at < end) {
+ val character = code[at]
+ if (character.isWhitespace() || character == '<') return start + 1
+ if (character == '>') {
+ if (!addressed) return start + 1
+ emit(start, at + 1, Kind.METADATA)
+ return at + 1
+ }
+ if (character == ':' || character == '@') addressed = true
+ at++
+ }
+ return start + 1
+ }
+
+ /**
+ * A bare `scheme://…` written in prose, or null if one does not start here.
+ *
+ * A scheme and `://` rather than a list of them, so `ftp`, `file` and `ssh` need no entry, and
+ * the pair of colons is what makes the match unambiguous enough to draw without a closer.
+ *
+ * Where it ends is the part worth stating: the sentence's punctuation is not the address, so a
+ * trailing `.` or `,` is given back, and so is a closing bracket unless one opened inside the
+ * URL -- otherwise a link in parentheses loses its `)` to the address. A pipe stops it too,
+ * because a URL in a table cell must not swallow the cell's edge.
+ */
+ private fun url(start: Int, end: Int): Int? {
+ if (start > 0 && isWord(code[start - 1])) return null
+ var scheme = start
+ while (scheme < end && code[scheme].isLetter()) scheme++
+ if (scheme == start || !code.startsWith("://", scheme)) return null
+ val body = scheme + 3
+ var at = body
+ var openers = 0
+ var closers = 0
+ while (at < end && !code[at].isWhitespace() && code[at] !in URL_STOPS) {
+ if (code[at] == '(') openers++ else if (code[at] == ')') closers++
+ at++
+ }
+ while (at > body) {
+ val last = code[at - 1]
+ if (last in URL_TRAILING) at--
+ else if (last == ')' && closers > openers) {
+ closers--
+ at--
+ } else break
+ }
+ if (at == body) return null
+ emit(start, at, Kind.METADATA)
+ return at
+ }
+
/**
* `*emph*`, `**strong**`, `_emph_` and `~~struck~~`, drawn markers and all.
*
diff --git a/app/androidApp/src/test/kotlin/com/example/aiapp/HighlighterTest.kt b/app/androidApp/src/test/kotlin/com/example/aiapp/HighlighterTest.kt
index 108c633..ab4f529 100644
--- a/app/androidApp/src/test/kotlin/com/example/aiapp/HighlighterTest.kt
+++ b/app/androidApp/src/test/kotlin/com/example/aiapp/HighlighterTest.kt
@@ -255,6 +255,78 @@ class HighlighterTest {
assertSpans(code, Language.MARKDOWN, Kind.METADATA, "(PLAN.md)")
}
+ @Test
+ fun `a table is found by its delimiter row, and pipes elsewhere are plain`() {
+ val code = "| a | b |\n|---|---|\n| 1 | 2 |\n\nrun a | b in a paragraph"
+ assertSpans(
+ code,
+ Language.MARKDOWN,
+ Kind.MARK,
+ "|",
+ "|",
+ "|",
+ "|---|---|",
+ "|",
+ "|",
+ "|",
+ )
+ }
+
+ @Test
+ fun `a table without outer pipes still colours, and the table ends with the rows`() {
+ val code = "a | b\n--- | ---\nnot a row"
+ assertSpans(code, Language.MARKDOWN, Kind.MARK, "|", "--- | ---")
+ }
+
+ /**
+ * The tag in the last case is not an autolink and is left plain, but the address inside it is
+ * still an address and the bare-URL pass finds it. That is the intended reading: raw HTML is
+ * not something this scanner knows, and a URL is a URL wherever it was written.
+ */
+ @Test
+ fun `an autolink colours and an HTML tag does not`() {
+ val code = "
and and and

"
+ assertSpans(
+ code,
+ Language.MARKDOWN,
+ Kind.METADATA,
+ "
",
+ "",
+ "http://x",
+ )
+ }
+
+ @Test
+ fun `a bare URL gives back the sentence's punctuation`() {
+ assertSpans(
+ "see https://example.com/a., and ssh://host/x)",
+ Language.MARKDOWN,
+ Kind.METADATA,
+ "https://example.com/a",
+ "ssh://host/x",
+ )
+ }
+
+ @Test
+ fun `a bracket a URL opened itself stays in it`() {
+ assertSpans(
+ "https://en.wikipedia.org/wiki/A_(b) here",
+ Language.MARKDOWN,
+ Kind.METADATA,
+ "https://en.wikipedia.org/wiki/A_(b)",
+ )
+ }
+
+ @Test
+ fun `a URL inside a link destination is not coloured twice`() {
+ assertSpans(
+ "[x](https://example.com)",
+ Language.MARKDOWN,
+ Kind.METADATA,
+ "(https://example.com)",
+ )
+ }
+
@Test
fun `a bracket with no destination after it is left plain`() {
assertSpans("an [aside] here", Language.MARKDOWN, Kind.MARK)
@@ -305,6 +377,12 @@ class HighlighterTest {
"1.",
"[x](",
"#######",
+ "|",
+ "|---|",
+ "<",
+ "<>",
+ "http://",
+ "a://",
"\n\n \n",
)
for (language in Language.entries) {
diff --git a/app/ui-sandbox.sh b/app/ui-sandbox.sh
index a735549..8da69d0 100755
--- a/app/ui-sandbox.sh
+++ b/app/ui-sandbox.sh
@@ -285,6 +285,13 @@ Not emphasis: a * b * c, and snake_case_name.
> quoted
+| column | what it holds |
+|--------|---------------|
+| one | a value |
+
+A link and a bare https://example.com/a., but run a | b
+in a paragraph has no table in it.
+
```rust
fn main() { println!("hello"); }
```