Group inline links and list items into browse lines
This commit is contained in:
1 parent
3dbd7f2554
commit
d01965c00d
16 files changed
+309
-15
No files matched your search
@@ -0,0 +1,64 @@
|
||||
// Copyright (c) 2026 Storm Dragon
|
||||
// Authors: Storm Dragon, Angus Kola
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package presentation
|
||||
|
||||
import "git.stormux.org/storm/cthulhu/internal/atspi"
|
||||
|
||||
// DocumentVisualLineForSpeech coalesces native fragments without losing inline
|
||||
// link roles. The returned copy is for speech only: braille and activation keep
|
||||
// the original objects and offsets. SourceContexts retains the privacy evidence.
|
||||
func DocumentVisualLineForSpeech(line atspi.DocumentLine) atspi.DocumentLine {
|
||||
result := line
|
||||
result.Contents = nil
|
||||
var previous atspi.ObjectID
|
||||
var heading *atspi.Snapshot
|
||||
for i, content := range line.Contents {
|
||||
snapshot := content.Snapshot
|
||||
var semantic atspi.ObjectID
|
||||
if snapshot.Role == "heading" && heading == nil {
|
||||
copy := snapshot
|
||||
heading = ©
|
||||
}
|
||||
ancestors := snapshot.BrailleContext
|
||||
if i < len(line.SourceContexts) {
|
||||
ancestors = line.SourceContexts[i]
|
||||
}
|
||||
if snapshot.Role == "link" {
|
||||
semantic = snapshot.ID()
|
||||
}
|
||||
for _, ancestor := range ancestors {
|
||||
if ancestor.Role == "heading" && heading == nil {
|
||||
copy := ancestor
|
||||
heading = ©
|
||||
}
|
||||
if ancestor.Role == "link" && !semantic.Valid() {
|
||||
snapshot, semantic = ancestor, ancestor.ID()
|
||||
}
|
||||
}
|
||||
value := content.RawText
|
||||
if value == "" {
|
||||
value = content.Text
|
||||
}
|
||||
if len(result.Contents) > 0 && semantic == previous {
|
||||
last := &result.Contents[len(result.Contents)-1]
|
||||
last.Text += content.Separator + value
|
||||
last.RawText = last.Text
|
||||
last.Snapshot.Misspelled = last.Snapshot.Misspelled || content.Snapshot.Misspelled
|
||||
} else {
|
||||
if !semantic.Valid() {
|
||||
// Plain spans share a text run; their container is not a role cue.
|
||||
snapshot.Role = "static"
|
||||
}
|
||||
snapshot.Misspelled = content.Snapshot.Misspelled
|
||||
result.Contents = append(result.Contents, atspi.DocumentContent{Snapshot: snapshot, Text: value, RawText: value})
|
||||
}
|
||||
previous = semantic
|
||||
}
|
||||
if heading != nil {
|
||||
heading.Name, heading.Text = "", ""
|
||||
result.Contents = append(result.Contents, atspi.DocumentContent{Snapshot: *heading})
|
||||
}
|
||||
return result
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
// Copyright (c) 2026 Storm Dragon
|
||||
// Authors: Storm Dragon, Angus Kola
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package presentation
|
||||
|
||||
import (
|
||||
"git.stormux.org/storm/cthulhu/internal/atspi"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestVisualLineSpeechRetainsInlineLinkRoles(t *testing.T) {
|
||||
plain := atspi.Snapshot{BusName: ":1.20", Path: "/plain", Role: "static"}
|
||||
link := atspi.Snapshot{BusName: ":1.20", Path: "/link", Role: "link", Visited: true}
|
||||
other := atspi.Snapshot{BusName: ":1.20", Path: "/other", Role: "link"}
|
||||
line := atspi.DocumentLine{
|
||||
Contents: []atspi.DocumentContent{
|
||||
{Snapshot: plain, Text: "Before "},
|
||||
{Snapshot: plain, Text: "split"}, {Snapshot: plain, Text: " link", RawText: " link"},
|
||||
{Snapshot: other, Text: "Next", Separator: " "},
|
||||
{Snapshot: plain, Text: " after"}, {Snapshot: plain, Text: "."},
|
||||
},
|
||||
SourceContexts: [][]atspi.Snapshot{{plain}, {plain, link}, {plain, link}, {other}, {plain}, {plain}},
|
||||
}
|
||||
spoken := DocumentVisualLineForSpeech(line)
|
||||
if got := DocumentLineText(spoken); got != "Before, split link, visited link, Next, link, after." {
|
||||
t.Fatalf("speech = %q", got)
|
||||
}
|
||||
if len(line.Contents) != 6 || line.Contents[1].Snapshot.Role != "static" {
|
||||
t.Fatal("speech conversion changed routing sources")
|
||||
}
|
||||
if len(spoken.SourceContexts) != 6 {
|
||||
t.Fatal("lost privacy sources")
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user