Expand embedded objects in braille document lines
Structural navigation to a heading whose native text is a single embedded-object placeholder for its child link showed only that placeholder in braille while speech used the expanded title. Braille line reads now expand embedded objects like speech capture, preserving RawText and native offsets; expanded lines carry no cell routes or native selection/attribute dots and only a line-start cursor. Frame assembly strips any remaining placeholders and falls back to displayed text so a placeholder never reaches the display. Regression tests cover placeholder-only, mixed, expanded, assembled, and empty headings plus native linked-heading and Firefox mixed-line braille. Maintainer confirms physical braille and speech agree on YouTube headings.
This commit is contained in:
1 parent
60f7f7a2e9
commit
8ce5e5cdad
7 files changed
+245
-21
No files matched your search
@@ -32,19 +32,27 @@ func brailleDocumentFrame(result review.Result, options config.BrailleOptions, c
|
||||
return linkedHeadingBraille(frame, result.DocumentPosition.LinkedHeading, options, textLength)
|
||||
}
|
||||
frame := braille.Frame{Cursor: -1, WordWrap: options.WordWrap}
|
||||
appendContent := func(source atspi.Snapshot, raw, separator string, sourceStart int) {
|
||||
// native reports that raw maps one-to-one onto native offsets at
|
||||
// sourceStart. Expanded or placeholder-stripped text keeps display
|
||||
// order but gets no routes or native-offset indicators, and only a
|
||||
// caret at the very start of the span can be placed honestly.
|
||||
appendContent := func(source atspi.Snapshot, raw, separator string, sourceStart int, native bool) {
|
||||
frame.Text += separator
|
||||
start := utf8.RuneCountInString(frame.Text)
|
||||
frame.Text += raw
|
||||
length := utf8.RuneCountInString(raw)
|
||||
if source.ID() == snapshot.ID() && result.DocumentPosition.Offset >= sourceStart && result.DocumentPosition.Offset <= sourceStart+length {
|
||||
frame.Cursor = start + result.DocumentPosition.Offset - sourceStart
|
||||
if native {
|
||||
frame.Cursor = start + result.DocumentPosition.Offset - sourceStart
|
||||
} else if result.DocumentPosition.Offset == sourceStart {
|
||||
frame.Cursor = start
|
||||
}
|
||||
}
|
||||
if source.ID().Valid() && source.Role != "password text" && length > 0 {
|
||||
if native && source.ID().Valid() && source.Role != "password text" && length > 0 {
|
||||
frame.Routes = append(frame.Routes, braille.TextRoute{Start: start, End: start + length,
|
||||
SourceStart: sourceStart, TextLength: length, BusName: source.BusName, Path: source.Path, SourceText: raw})
|
||||
}
|
||||
if options.SelectionIndicator != 0 && source.SelectionEnd > source.SelectionStart {
|
||||
if native && options.SelectionIndicator != 0 && source.SelectionEnd > source.SelectionStart {
|
||||
frame.Dots = append(frame.Dots, make([]byte, utf8.RuneCountInString(frame.Text)-len(frame.Dots))...)
|
||||
for offset := max(source.SelectionStart, sourceStart); offset < min(source.SelectionEnd, sourceStart+length); offset++ {
|
||||
frame.Dots[start+offset-sourceStart] |= byte(options.SelectionIndicator)
|
||||
@@ -60,7 +68,7 @@ func brailleDocumentFrame(result review.Result, options config.BrailleOptions, c
|
||||
frame.Dots[offset] |= byte(options.LinkIndicator)
|
||||
}
|
||||
}
|
||||
if len(configured) != 0 {
|
||||
if native && len(configured) != 0 {
|
||||
if len(source.TextAttributeRuns) != 0 {
|
||||
for _, run := range source.TextAttributeRuns {
|
||||
markTextAttributeRun(&frame, run, sourceStart, start, length, configured[0])
|
||||
@@ -79,12 +87,38 @@ func brailleDocumentFrame(result review.Result, options config.BrailleOptions, c
|
||||
if content.Snapshot.Role == "password text" {
|
||||
continue
|
||||
}
|
||||
appendContent(content.Snapshot, raw, content.Separator, content.Start)
|
||||
// A placeholder fragment stands for child objects presented
|
||||
// as their own contents; drop it instead of duplicating them.
|
||||
native := !strings.ContainsRune(raw, '\ufffc')
|
||||
if !native {
|
||||
raw = stripEmbeddedObjects(raw)
|
||||
}
|
||||
appendContent(content.Snapshot, raw, content.Separator, content.Start, native)
|
||||
}
|
||||
if strings.TrimSpace(frame.Text) == "" {
|
||||
// Nothing but placeholders survived assembly; fall back to
|
||||
// the displayed text speech would use for this object.
|
||||
if text := brailleDisplayedText(snapshot); text != "" {
|
||||
frame.Text, frame.Routes, frame.Dots = text, nil, nil
|
||||
if frame.Cursor > 0 {
|
||||
frame.Cursor = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
snapshot.SelectionStart, snapshot.SelectionEnd = line.SelectionStart, line.SelectionEnd
|
||||
snapshot.TextAttributeRuns = line.TextAttributeRuns
|
||||
appendContent(snapshot, strings.TrimRight(line.Text, "\r\n"), "", line.Start)
|
||||
text := strings.TrimRight(line.Text, "\r\n")
|
||||
native := !line.Expanded && !strings.ContainsRune(text, '\ufffc')
|
||||
if !native {
|
||||
text = stripEmbeddedObjects(text)
|
||||
if strings.TrimSpace(text) == "" {
|
||||
text = brailleDisplayedText(snapshot)
|
||||
}
|
||||
}
|
||||
if native {
|
||||
snapshot.SelectionStart, snapshot.SelectionEnd = line.SelectionStart, line.SelectionEnd
|
||||
snapshot.TextAttributeRuns = line.TextAttributeRuns
|
||||
}
|
||||
appendContent(snapshot, text, "", line.Start, native)
|
||||
}
|
||||
textLength := utf8.RuneCountInString(frame.Text)
|
||||
if frame.Cursor < 0 || frame.Cursor > textLength {
|
||||
@@ -163,6 +197,23 @@ func brailleFocusFrameWithOptions(s atspi.Snapshot, options config.BrailleOption
|
||||
return braille_context_frame(braille_object_frame(s, options, configured...), s, options)
|
||||
}
|
||||
|
||||
// Embedded-object placeholders stand for child accessibles in native text.
|
||||
// They are structure, not displayed characters, and must never reach a frame.
|
||||
func stripEmbeddedObjects(value string) string {
|
||||
return strings.ReplaceAll(value, "\ufffc", "")
|
||||
}
|
||||
|
||||
// brailleDisplayedText returns the text speech uses for an object whose
|
||||
// native line is only placeholders: expanded text first, then the name.
|
||||
func brailleDisplayedText(snapshot atspi.Snapshot) string {
|
||||
for _, candidate := range []string{snapshot.Text, snapshot.Name} {
|
||||
if candidate = strings.TrimSpace(candidate); candidate != "" && !strings.ContainsRune(candidate, '\ufffc') {
|
||||
return candidate
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func braille_object_frame(s atspi.Snapshot, options config.BrailleOptions, configured ...config.TextAttributeSettings) braille.Frame {
|
||||
if frame, handled := libreOfficeBrailleFrame(s, options); handled {
|
||||
return frame
|
||||
@@ -174,6 +225,19 @@ func braille_object_frame(s atspi.Snapshot, options config.BrailleOptions, confi
|
||||
label = ""
|
||||
}
|
||||
line := strings.TrimRight(s.BrailleLine, "\r\n")
|
||||
// A line containing embedded-object placeholders cannot map cells
|
||||
// to native offsets. Prefer the expanded capture of the same line,
|
||||
// then real surrounding text, then the accessible name.
|
||||
native := !strings.ContainsRune(line, '\ufffc')
|
||||
if !native {
|
||||
if expanded := strings.TrimSpace(s.Text); expanded != "" && !strings.ContainsRune(expanded, '\ufffc') {
|
||||
line = expanded
|
||||
} else if stripped := stripEmbeddedObjects(line); strings.TrimSpace(stripped) != "" {
|
||||
line = stripped
|
||||
} else {
|
||||
line = brailleDisplayedText(s)
|
||||
}
|
||||
}
|
||||
if label == line {
|
||||
label = ""
|
||||
}
|
||||
@@ -186,7 +250,11 @@ func braille_object_frame(s atspi.Snapshot, options config.BrailleOptions, confi
|
||||
cursor := -1
|
||||
relative := s.BrailleCaret - s.BrailleLineStart
|
||||
if s.TextCaretKnown && relative >= 0 && relative <= utf8.RuneCountInString(s.BrailleLine) {
|
||||
cursor = utf8.RuneCountInString(prefix) + min(relative, utf8.RuneCountInString(line))
|
||||
if native {
|
||||
cursor = utf8.RuneCountInString(prefix) + min(relative, utf8.RuneCountInString(line))
|
||||
} else if relative == 0 {
|
||||
cursor = utf8.RuneCountInString(prefix)
|
||||
}
|
||||
}
|
||||
if role != "terminal" && role != "heading" && !options.DisableEOL {
|
||||
text += " $l"
|
||||
@@ -194,7 +262,7 @@ func braille_object_frame(s atspi.Snapshot, options config.BrailleOptions, confi
|
||||
text += " "
|
||||
}
|
||||
frame := braille.Frame{Text: text, Cursor: cursor, WordWrap: options.WordWrap}
|
||||
if options.SelectionIndicator != 0 && s.SelectionEnd > s.SelectionStart {
|
||||
if native && options.SelectionIndicator != 0 && s.SelectionEnd > s.SelectionStart {
|
||||
frame.Dots = make([]byte, utf8.RuneCountInString(frame.Text))
|
||||
start := utf8.RuneCountInString(prefix)
|
||||
for source := max(s.SelectionStart, s.BrailleLineStart); source < min(s.SelectionEnd, s.BrailleLineStart+utf8.RuneCountInString(line)); source++ {
|
||||
@@ -216,7 +284,7 @@ func braille_object_frame(s atspi.Snapshot, options config.BrailleOptions, confi
|
||||
break
|
||||
}
|
||||
}
|
||||
if len(configured) != 0 {
|
||||
if native && len(configured) != 0 {
|
||||
if len(s.TextAttributeRuns) != 0 {
|
||||
for _, run := range s.TextAttributeRuns {
|
||||
markTextAttributeRun(&frame, run, s.BrailleLineStart, utf8.RuneCountInString(prefix), utf8.RuneCountInString(line), configured[0])
|
||||
@@ -226,7 +294,7 @@ func braille_object_frame(s atspi.Snapshot, options config.BrailleOptions, confi
|
||||
}
|
||||
}
|
||||
// Only actual text and its EOL marker route; labels and state suffixes do not.
|
||||
if s.TextCaretKnown && s.ID().Valid() {
|
||||
if native && s.TextCaretKnown && s.ID().Valid() {
|
||||
frame.Routes = []braille.TextRoute{{Start: utf8.RuneCountInString(prefix), End: utf8.RuneCountInString(text),
|
||||
SourceStart: s.BrailleLineStart, TextLength: utf8.RuneCountInString(line),
|
||||
BusName: s.BusName, Path: s.Path, SourceText: s.BrailleLine}}
|
||||
@@ -341,7 +409,7 @@ func braille_object_frame(s atspi.Snapshot, options config.BrailleOptions, confi
|
||||
case "progress bar":
|
||||
text = brailleJoin(name, value, roleName)
|
||||
default:
|
||||
displayed := s.Text
|
||||
displayed := stripEmbeddedObjects(s.Text)
|
||||
if displayed == name {
|
||||
displayed = ""
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user