Expanding embedded-object placeholders changes the displayed braille text
without changing native AT-SPI offsets. Several consumers still validated
native positions against the expanded string, which could reject a valid
line end after contraction, accept an offset beyond the native line after
expansion, or drop selection and text-attribute indicators on a plain-text
fragment following an embedded object.
Use RawText for native-offset validation and fragment comparisons whenever
a braille line is expanded. Apply the same rule to parent/child native-caret
handoff: identical displayed text must not authorize an offset transfer
when the underlying native strings differ. Expanded spans retain their
existing conservative presentation without native cell routes.
Restrict the assembled-line displayed-text fallback to lines from which
embedded-object placeholders were actually removed. Ordinary whitespace-only
lines now retain their spaces, cursor position, and native routing instead
of being replaced with the object's text or accessible name.
Add regressions for contracted and expanded native offset bounds, suffix
selection and attribute preservation, immutable line enrichment, ambiguous
caret transfer, and whitespace-only line routing. The regressions reproduced
the review findings before the fixes and pass afterward.
Validation:
- Full Go suite before fixes; full race and BrlAPI suites after fixes.
- Focused Braille and native-caret regressions, including race checks.
- go vet ./...; reader and BrlAPI helper builds; parity-ledger validation.
- Private Chromium linked-heading, selection, and browse-mode checks.
- Private Firefox baseline and linked-heading checks.
- git diff --check.
The Chromium fragment harness still fails its embedded-editor navigation
step, and the Firefox harness still times out in its New Tab test. Both
failures reproduce on the parent of the reviewed commit, 8ce5e5c, as well.
These failures are not claimed as passing checks.
The active reader was not replaced. Physical keyboard, audible speech, and
Braille acceptance remain for maintainer testing: check linked headings,
mixed inline-link text, selection indicators after links, and ordinary or
blank-line cursor routing in both browser families before submission.
84 lines
3.3 KiB
Go
84 lines
3.3 KiB
Go
// Copyright (c) 2026 Storm Dragon
|
|
// Authors: Storm Dragon, Angus Kola
|
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
package review
|
|
|
|
import (
|
|
"context"
|
|
"time"
|
|
"unicode/utf8"
|
|
|
|
"git.stormux.org/storm/cthulhu/internal/atspi"
|
|
)
|
|
|
|
// readDocumentBraille reads the accepted line and selection ranges for its
|
|
// inline fragments. Optional display reads never fail or repeat navigation.
|
|
func (worker *Worker) readDocumentBraille(ctx context.Context, result Result) Result {
|
|
if !worker.documentBraille.Load() || !result.Moved || result.Err != nil || result.CaretErr != nil ||
|
|
!worker.CurrentDocumentResult(result) || worker.focus() != result.DocumentFocus ||
|
|
result.DocumentCaretCommand >= TableLeft || result.DocumentPosition.Snapshot.Role == "password text" {
|
|
return result
|
|
}
|
|
// Explicitly named links are atomic browse objects. Their native text may
|
|
// contain only a numeric badge; braille must use the same accessible label
|
|
// as speech rather than enriching it with that incomplete child text.
|
|
if snapshot := result.DocumentPosition.Snapshot; snapshot.Role == "link" && wholeCaretNode(snapshot) {
|
|
return result
|
|
}
|
|
reader, ok := worker.nativeCapturerFor(result.DocumentFocus).(DocumentCaretReader)
|
|
if !ok || !result.DocumentPosition.Object.Valid() {
|
|
return result
|
|
}
|
|
// Display enrichment must not hold ordinary speech/navigation for the
|
|
// longer whole-document capture timeout.
|
|
deadline, cancel := context.WithTimeout(ctx, min(worker.captureTimeout, 100*time.Millisecond))
|
|
defer cancel()
|
|
line, err := reader.ReadDocumentCaret(deadline, result.DocumentPosition.Object, result.DocumentPosition.Offset, atspi.DocumentCaretBrailleLine)
|
|
if err != nil {
|
|
result.DocumentBrailleErr = err
|
|
return result
|
|
}
|
|
if line.Start < 0 || result.DocumentPosition.Offset < line.Start ||
|
|
result.DocumentPosition.Offset-line.Start > utf8.RuneCountInString(documentBrailleNativeText(line)) || line.Text == "" {
|
|
result.DocumentBrailleErr = atspi.ErrNoDocumentCaret
|
|
return result
|
|
}
|
|
result.DocumentBrailleLine = &line
|
|
if assembled := result.DocumentCaretLine; assembled != nil {
|
|
copyLine := *assembled
|
|
copyLine.Contents = append([]atspi.DocumentContent(nil), assembled.Contents...)
|
|
for index := range copyLine.Contents {
|
|
content := ©Line.Contents[index]
|
|
content.Snapshot.SelectionStart, content.Snapshot.SelectionEnd = 0, 0
|
|
selected := line
|
|
if content.Snapshot.ID() != result.DocumentPosition.Object.ID() {
|
|
if !content.Reference.Valid() {
|
|
continue
|
|
}
|
|
selected, err = reader.ReadDocumentCaret(deadline, content.Reference, content.Start, atspi.DocumentCaretBrailleLine)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
}
|
|
start, end := content.Start-selected.Start, content.Start-selected.Start+utf8.RuneCountInString(content.RawText)
|
|
runes := []rune(documentBrailleNativeText(selected))
|
|
if start < 0 || end > len(runes) || string(runes[start:end]) != content.RawText {
|
|
continue
|
|
}
|
|
content.Snapshot.SelectionStart, content.Snapshot.SelectionEnd = selected.SelectionStart, selected.SelectionEnd
|
|
content.Snapshot.TextAttributeRuns = selected.TextAttributeRuns
|
|
}
|
|
result.DocumentCaretLine = ©Line
|
|
}
|
|
return result
|
|
}
|
|
|
|
// Braille expansion changes display characters, never the native offset domain.
|
|
func documentBrailleNativeText(line atspi.DocumentCaretText) string {
|
|
if line.Expanded {
|
|
return line.RawText
|
|
}
|
|
return line.Text
|
|
}
|