Add phase 4: markdown highlighting, tap to toggle, JetBrains Mono

Highlighting styles markers in place rather than hiding them, so the
offset mapping is the identity and cursor maths needs no translation.
The span finding is deliberately free of Compose types, which puts the
offset arithmetic under 25 JVM unit tests instead of under a phone.
Several cases come straight from the corpus: `*arr` must not italicise,
`- [ ]` must not parse as a link, snake_case must be left alone.

Tap to toggle did nothing on the first attempt, and the reason was not
where it looked. A diagnostic showed the handler firing and resolving
exactly the right offsets, so detection was fine. The release was the
problem: BasicTextField consumes the tap, and waitForUpOrCancellation
returns null for a consumed pointer, so the press arrived and the release
never did. Leaving the press unconsumed, to protect ordinary cursor
placement, was what handed the gesture away. Now the press is claimed in
the Initial pass, but only once the offset is confirmed to be inside a
bracket span, so every other tap is untouched.

The same diagnostic disproved a plausible worry, that a scrolled field
would mis-resolve taps because getOffsetForPosition works in layout
coordinates and pointerInput reports viewport ones. The offsets were
correct, so a correct piece of code was left alone.

Bundled the no-ligature cut of JetBrains Mono. A ligature draws two
characters as one glyph, which is at odds with an editor built on every
character staying visible. Static faces rather than the variable file,
which needs an experimental API, and a real bold rather than a
synthesised one. OFL text is in licenses/.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PU5ZFfQFtDTFqqhvMTWdGH
This commit is contained in:
Connor JohnstoneandClaude Opus 5 committed 2026-09-09 20:21:56 -04:00
1 parent 1e91ff8f07
commit bb66567725
9 files changed
+536 -5

No files matched your search

+29
View File
@@ -264,6 +264,35 @@ A second bug in the same few lines: `Modifier.safeDrawingPadding()` in the
activity **already includes the IME inset**, and the screen then applied
`imePadding()` on top, so the keyboard inset was counted twice.
## An unconsumed press means waitForUpOrCancellation never returns
Tap to toggle did nothing at all, while a diagnostic log showed the gesture
handler firing on every tap and resolving the correct offset. Tapping the line
`- [ ] Take some time to build out...` produced offsets 289 through 292, and
that line's bracket span is exactly 289..292, so detection was never the
problem.
The problem was two lines further on. `BasicTextField` handles the tap itself
and consumes the pointer, and `waitForUpOrCancellation` returns **null** for a
pointer somebody else has consumed. The press arrived, the release never did,
and nothing ever toggled.
This was self inflicted. The press was deliberately left unconsumed so that
ordinary cursor placement and scrolling would keep working, and that choice
handed the gesture to the text field. The fix is to claim the press in
`PointerEventPass.Initial`, which runs outer first, but only once
`checkboxSpanAt` has confirmed the press actually landed on a box. Every other
tap stays unclaimed and behaves exactly as before.
The cost is that a scroll begun with a finger already on a `[ ]` will not
scroll. The target is three characters wide, so this is a fair trade.
Worth noting what was *not* wrong. `getOffsetForPosition` takes layout
coordinates while `pointerInput` reports viewport coordinates, so a scrolled
field looked like it should mis-resolve taps. The logged offsets show it does
not. Instrumenting first turned a plausible theory into a measured fact and
saved rewriting a correct piece of code.
## Reconciliation, phase 0, completed 2026-09-09
| File | Resolution |
@@ -0,0 +1,104 @@
package com.example.todo.editor
/**
* Finding markup in plain text. Deliberately free of Compose types so the
* offset arithmetic, which is the part that actually goes wrong, can be tested
* on the JVM rather than on a phone.
*
* Every character is kept, markers included. That is what lets the editor use
* an identity OffsetMapping, and it is why cursor and selection maths stay
* simply correct.
*/
enum class Mark { H1, H2, H3, H4, BOLD, ITALIC, CODE, STRIKE, LINK, DONE }
/** Half open: [start, end). */
data class MarkSpan(val start: Int, val end: Int, val mark: Mark)
private val HEADING = Regex("""^(#{1,4})\s""")
private val DONE_LINE = Regex("""^\s*[-*+]\s+\[[xX]]""")
// Ordered by priority. Code wins over everything, so a `**` inside backticks
// stays literal. Two-character markers are matched before their one-character
// counterparts, otherwise `**bold**` would read as an empty italic.
private val INLINE = listOf(
Regex("""`[^`\n]+`""") to Mark.CODE,
Regex("""\[[^\[\]\n]*]\([^)\n]*\)""") to Mark.LINK,
Regex("""\*\*[^*\n]+\*\*""") to Mark.BOLD,
Regex("""~~[^~\n]+~~""") to Mark.STRIKE,
Regex("""(?<!~)~[^~\n]+~(?!~)""") to Mark.STRIKE,
Regex("""(?<![*\w])\*[^*\n]+\*(?![*\w])""") to Mark.ITALIC,
Regex("""(?<![_\w])_[^_\n]+_(?![_\w])""") to Mark.ITALIC,
)
fun scan(text: String): List<MarkSpan> {
val spans = mutableListOf<MarkSpan>()
forEachLine(text) { start, end ->
val line = text.substring(start, end)
HEADING.find(line)?.let {
spans += MarkSpan(start, end, when (it.groupValues[1].length) {
1 -> Mark.H1
2 -> Mark.H2
3 -> Mark.H3
else -> Mark.H4
})
}
if (DONE_LINE.containsMatchIn(line)) spans += MarkSpan(start, end, Mark.DONE)
}
// Line marks do not consume, so inline styling still applies inside a
// heading or a finished item.
val taken = BooleanArray(text.length)
for ((re, mark) in INLINE) {
for (m in re.findAll(text)) {
val r = m.range
if (r.isEmpty() || (r.first..r.last).any { taken[it] }) continue
for (i in r.first..r.last) taken[i] = true
spans += MarkSpan(r.first, r.last + 1, mark)
}
}
return spans
}
private inline fun forEachLine(text: String, body: (Int, Int) -> Unit) {
var start = 0
while (start <= text.length) {
val nl = text.indexOf('\n', start).let { if (it < 0) text.length else it }
body(start, nl)
if (nl == text.length) return
start = nl + 1
}
}
/** The `[ ]` or `[x]` of a checkbox, as a half open range over the source. */
data class CheckboxSpan(val start: Int, val end: Int, val checked: Boolean)
private val CHECKBOX = Regex("""^(\s*[-*+]\s+)\[([ xX?])]""")
/**
* The checkbox whose bracket span covers [offset], or null.
*
* The span is the three characters of the box alone, not the whole line, so a
* tap anywhere else keeps its ordinary meaning of placing the cursor.
*/
fun checkboxSpanAt(text: String, offset: Int): CheckboxSpan? {
if (offset < 0 || offset > text.length) return null
val lineStart = text.lastIndexOf('\n', (offset - 1).coerceAtLeast(0))
.let { if (it < 0) 0 else it + 1 }
val lineEnd = text.indexOf('\n', lineStart).let { if (it < 0) text.length else it }
val m = CHECKBOX.find(text.substring(lineStart, lineEnd)) ?: return null
val start = lineStart + m.groupValues[1].length
val end = start + 3
if (offset !in start..end) return null
return CheckboxSpan(start, end, m.groupValues[2].lowercase() == "x")
}
/**
* Flips one checkbox. `[ ]` and `[x]` are both three characters wide, so the
* text length never changes and every offset after it survives untouched.
*/
fun toggleCheckbox(text: String, span: CheckboxSpan): String =
text.substring(0, span.start) +
(if (span.checked) "[ ]" else "[x]") +
text.substring(span.end)
@@ -0,0 +1,68 @@
package com.example.todo.editor
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.input.OffsetMapping
import androidx.compose.ui.text.input.TransformedText
import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.unit.TextUnit
import androidx.compose.ui.unit.sp
/**
* Styles markdown in place, leaving every marker visible.
*
* Because no character is added or removed the mapping is the identity, which
* is the whole reason for styling in source mode: cursor and selection offsets
* need no translation and so cannot drift.
*/
class MarkdownHighlighter(
private val onSurface: Color,
private val codeBackground: Color,
private val linkColor: Color,
private val baseSize: TextUnit = 14.sp,
) : VisualTransformation {
// filter runs on every keystroke over the whole document. Notes are a few
// kilobytes so this is cheap, but recomposition calls it far more often
// than the text actually changes.
private var lastSource: String? = null
private var lastResult: AnnotatedString? = null
override fun filter(text: AnnotatedString): TransformedText {
val source = text.text
val styled = lastResult.takeIf { lastSource == source } ?: annotate(source).also {
lastSource = source
lastResult = it
}
return TransformedText(styled, OffsetMapping.Identity)
}
private fun annotate(source: String) = AnnotatedString.Builder(source).apply {
// Line marks first so inline styling layers on top of them.
val spans = scan(source).sortedBy { it.mark.ordinal }
for (s in spans) addStyle(styleFor(s.mark), s.start, s.end)
}.toAnnotatedString()
private fun styleFor(mark: Mark): SpanStyle = when (mark) {
Mark.H1 -> SpanStyle(fontWeight = FontWeight.Bold, fontSize = baseSize * 1.45f)
Mark.H2 -> SpanStyle(fontWeight = FontWeight.Bold, fontSize = baseSize * 1.28f)
Mark.H3 -> SpanStyle(fontWeight = FontWeight.Bold, fontSize = baseSize * 1.14f)
Mark.H4 -> SpanStyle(fontWeight = FontWeight.Bold, fontSize = baseSize)
// Dimmed and struck, so a finished item reads as done without hiding it.
Mark.DONE -> SpanStyle(
color = onSurface.copy(alpha = 0.45f),
textDecoration = TextDecoration.LineThrough,
)
Mark.BOLD -> SpanStyle(fontWeight = FontWeight.Bold)
Mark.ITALIC -> SpanStyle(fontStyle = FontStyle.Italic)
// The body is already monospace, so a code span is distinguished by its
// tint alone. No colour is set, so this does not undo a dimmed line.
Mark.CODE -> SpanStyle(background = codeBackground)
Mark.STRIKE -> SpanStyle(textDecoration = TextDecoration.LineThrough)
Mark.LINK -> SpanStyle(color = linkColor)
}
}
@@ -0,0 +1,21 @@
package com.example.todo.theme
import androidx.compose.ui.text.font.Font
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import com.example.todo.R
/**
* JetBrains Mono, bundled rather than fetched so the editor looks the same with
* no network. Monospace because the notes nest by indentation, and proportional
* glyphs stop those columns lining up.
*
* The NL cut has no ligatures, which is the point: a ligature draws two
* characters as one glyph, and this editor is built on every character staying
* visible and every offset staying honest. A real bold face is bundled too,
* because a synthesised one looks smeared beside the regular.
*/
val JetBrainsMono = FontFamily(
Font(R.font.jetbrains_mono_nl_regular, FontWeight.Normal),
Font(R.font.jetbrains_mono_nl_bold, FontWeight.Bold),
)
@@ -1,5 +1,8 @@
package com.example.todo.ui.note
import androidx.compose.foundation.gestures.awaitEachGesture
import androidx.compose.foundation.gestures.awaitFirstDown
import androidx.compose.foundation.gestures.waitForUpOrCancellation
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
@@ -12,6 +15,7 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
@@ -19,12 +23,20 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.input.pointer.PointerEventPass
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.text.TextLayoutResult
import androidx.compose.ui.text.TextRange
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import com.example.todo.editor.MarkdownHighlighter
import com.example.todo.theme.JetBrainsMono
import com.example.todo.editor.checkboxSpanAt
import com.example.todo.editor.toggleCheckbox
@Composable
fun NoteScreen(viewModel: NoteViewModel = viewModel(), modifier: Modifier = Modifier) {
@@ -35,6 +47,32 @@ fun NoteScreen(viewModel: NoteViewModel = viewModel(), modifier: Modifier = Modi
return
}
val scheme = MaterialTheme.colorScheme
val highlighter = remember(scheme) {
MarkdownHighlighter(
onSurface = scheme.onBackground,
codeBackground = scheme.surfaceVariant,
linkColor = scheme.primary,
)
}
// The editor owns cursor and selection; the view model owns the text that
// gets synced. They are only forced back together when the text changes
// from underneath, which means a fetch replaced it.
val field = remember { mutableStateOf(TextFieldValue(state.text)) }
LaunchedEffect(state.text) {
if (state.text != field.value.text) {
val where = field.value.selection.start.coerceIn(0, state.text.length)
field.value = TextFieldValue(state.text, TextRange(where))
}
}
val layout = remember { mutableStateOf<TextLayoutResult?>(null) }
fun update(new: TextFieldValue) {
field.value = new
viewModel.onTextChange(new.text)
}
Column(modifier.fillMaxSize().padding(12.dp)) {
Row(
Modifier.fillMaxWidth().padding(bottom = 8.dp),
@@ -52,11 +90,39 @@ fun NoteScreen(viewModel: NoteViewModel = viewModel(), modifier: Modifier = Modi
}
BasicTextField(
value = state.text,
onValueChange = viewModel::onTextChange,
modifier = Modifier.fillMaxSize(),
value = field.value,
onValueChange = ::update,
onTextLayout = { layout.value = it },
visualTransformation = highlighter,
modifier = Modifier
.fillMaxSize()
.pointerInput(Unit) {
awaitEachGesture {
// Claimed in the Initial pass, which runs outer first,
// and only for a press that actually lands on a box.
// Leaving it unclaimed lets the text field consume it,
// and a consumed pointer makes waitForUpOrCancellation
// return null, so the release never arrives here.
val down = awaitFirstDown(
requireUnconsumed = false,
pass = PointerEventPass.Initial,
)
val at = layout.value ?: return@awaitEachGesture
val text = field.value.text
val span = checkboxSpanAt(text, at.getOffsetForPosition(down.position))
?: return@awaitEachGesture
down.consume()
val up = waitForUpOrCancellation() ?: return@awaitEachGesture
up.consume()
// Same box on release, so a tap rather than a drag.
if (checkboxSpanAt(text, at.getOffsetForPosition(up.position)) == span) {
update(field.value.copy(text = toggleCheckbox(text, span)))
}
}
},
textStyle = TextStyle(
fontFamily = FontFamily.Monospace,
fontFamily = JetBrainsMono,
fontSize = 14.sp,
color = MaterialTheme.colorScheme.onBackground,
),
@@ -0,0 +1,150 @@
package com.example.todo.editor
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Test
class MarkdownTest {
private fun marks(s: String) = scan(s).map { it.mark }.toSet()
private fun textOf(s: String, m: Mark) =
scan(s).filter { it.mark == m }.map { s.substring(it.start, it.end) }
@Test fun `markers are kept, so spans cover them`() {
assertEquals(listOf("**bold**"), textOf("a **bold** b", Mark.BOLD))
}
@Test fun `heading level sets the mark`() {
assertTrue(Mark.H1 in marks("# 2026-09-09"))
assertTrue(Mark.H2 in marks("## To-Do"))
assertTrue(Mark.H3 in marks("### Personal"))
assertTrue(Mark.H4 in marks("#### Computer"))
}
@Test fun `five hashes is not a heading`() {
assertTrue(marks("##### too deep").none { it in setOf(Mark.H1, Mark.H2, Mark.H3, Mark.H4) })
}
@Test fun `a finished item marks the whole line`() {
val s = "- [x] Climb"
val done = scan(s).single { it.mark == Mark.DONE }
assertEquals(s, s.substring(done.start, done.end))
}
@Test fun `an unfinished item is not marked done`() {
assertTrue(Mark.DONE !in marks("- [ ] Climb"))
assertTrue(Mark.DONE !in marks(" - [?] Cincinnati Trash"))
}
@Test fun `done marks only its own line`() {
val s = "- [x] done\n- [ ] open"
val done = scan(s).single { it.mark == Mark.DONE }
assertEquals("- [x] done", s.substring(done.start, done.end))
}
@Test fun `bold wins over italic`() {
assertEquals(listOf("**bold**"), textOf("**bold**", Mark.BOLD))
assertTrue(Mark.ITALIC !in marks("**bold**"))
}
@Test fun `both tilde forms strike`() {
assertEquals(listOf("~one~"), textOf("~one~", Mark.STRIKE))
assertEquals(listOf("~~two~~"), textOf("~~two~~", Mark.STRIKE))
}
@Test fun `code beats other markup inside it`() {
val s = "`a **b** c`"
assertEquals(listOf(s), textOf(s, Mark.CODE))
assertTrue(Mark.BOLD !in marks(s))
}
@Test fun `links are one span`() {
assertEquals(listOf("[text](http://x)"), textOf("see [text](http://x) here", Mark.LINK))
}
@Test fun `a checkbox is not mistaken for a link`() {
assertTrue(Mark.LINK !in marks("- [ ] not a link"))
assertTrue(Mark.LINK !in marks("- [x] also not"))
}
@Test fun `a lone asterisk does not italicise`() {
// Real line from the corpus.
val s = "- [x] Have Claude go through my stranded downloads and get them in *arr"
assertTrue(Mark.ITALIC !in marks(s))
}
@Test fun `underscores inside a word do not italicise`() {
assertTrue(Mark.ITALIC !in marks("snake_case_name"))
}
@Test fun `markup does not run across lines`() {
assertTrue(Mark.BOLD !in marks("**a\nb**"))
}
@Test fun `inline markup still applies inside a heading`() {
assertTrue(Mark.CODE in marks("## the `CspRouter`"))
}
@Test fun `spans stay inside the text`() {
val s = "# h\n- [x] d **b** `c` ~s~ [l](u)\n"
assertTrue(scan(s).all { it.start >= 0 && it.end <= s.length && it.start < it.end })
}
}
class CheckboxTest {
@Test fun `offset inside the box finds it`() {
val s = "- [ ] Climb"
val span = checkboxSpanAt(s, 3)!!
assertEquals("[ ]", s.substring(span.start, span.end))
assertEquals(false, span.checked)
}
@Test fun `offset on the text does not find it`() {
assertNull(checkboxSpanAt("- [ ] Climb", 8))
}
@Test fun `offset on the bullet does not find it`() {
assertNull(checkboxSpanAt("- [ ] Climb", 0))
}
@Test fun `indented checkboxes resolve against their own line`() {
val s = "## To-Do\n - [x] Runway"
val span = checkboxSpanAt(s, s.indexOf("[x]") + 1)!!
assertEquals("[x]", s.substring(span.start, span.end))
assertEquals(true, span.checked)
}
@Test fun `toggling preserves length and every later offset`() {
val s = "- [ ] a\n- [x] b"
val span = checkboxSpanAt(s, 3)!!
val out = toggleCheckbox(s, span)
assertEquals(s.length, out.length)
assertEquals("- [x] a\n- [x] b", out)
}
@Test fun `toggling back is the identity`() {
val s = " - [x] done"
val once = toggleCheckbox(s, checkboxSpanAt(s, 5)!!)
val twice = toggleCheckbox(once, checkboxSpanAt(once, 5)!!)
assertEquals(s, twice)
}
@Test fun `a question box toggles to done`() {
val s = "- [?] unsure"
assertEquals("- [x] unsure", toggleCheckbox(s, checkboxSpanAt(s, 3)!!))
}
@Test fun `out of range offsets are safe`() {
assertNull(checkboxSpanAt("- [ ] a", -1))
assertNull(checkboxSpanAt("- [ ] a", 999))
assertNull(checkboxSpanAt("", 0))
}
@Test fun `a line that is not a checkbox yields nothing`() {
assertNull(checkboxSpanAt("## To-Do", 3))
assertNull(checkboxSpanAt(" - plain bullet", 4))
}
}
+93
View File
@@ -0,0 +1,93 @@
Copyright 2020 The JetBrains Mono Project Authors (https://github.com/JetBrains/JetBrainsMono)
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
https://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.