Reset master to testing content
This commit is contained in:
1 parent
0f2587b719
commit
42afa2a56c
1483 files changed
+145308
-938069
No files matched your search
@@ -0,0 +1,169 @@
|
||||
// Copyright (c) 2026 Storm Dragon
|
||||
// Authors: Storm Dragon, Angus Kola
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package ocr
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Unit uint8
|
||||
|
||||
const (
|
||||
Character Unit = iota + 1
|
||||
WordUnit
|
||||
LineUnit
|
||||
PageUnit
|
||||
LineStart
|
||||
LineEnd
|
||||
DocumentStart
|
||||
DocumentEnd
|
||||
)
|
||||
|
||||
// Cursor owns a virtual rune offset. It has no native accessible identity and
|
||||
// never moves application focus. A session discards it on retirement.
|
||||
type Cursor struct {
|
||||
result Result
|
||||
text []rune
|
||||
lines []Span
|
||||
lineTexts []string
|
||||
offset, column int
|
||||
}
|
||||
|
||||
type Position struct {
|
||||
Offset int
|
||||
Text string
|
||||
Line string
|
||||
LineStart int
|
||||
Cursor int // Rune offset within Line, including its end position.
|
||||
Moved bool
|
||||
}
|
||||
|
||||
type ReadLine struct {
|
||||
Offset int
|
||||
Text string
|
||||
}
|
||||
|
||||
func NewCursor(result Result) *Cursor {
|
||||
cursor := &Cursor{result: result, text: []rune(result.Text), lineTexts: strings.Split(result.Text, "\n")}
|
||||
start := 0
|
||||
for index, char := range cursor.text {
|
||||
if char == '\n' {
|
||||
cursor.lines = append(cursor.lines, Span{start, index})
|
||||
start = index + 1
|
||||
}
|
||||
}
|
||||
cursor.lines = append(cursor.lines, Span{start, len(cursor.text)})
|
||||
return cursor
|
||||
}
|
||||
|
||||
func (cursor *Cursor) line_index() int {
|
||||
return sort.Search(len(cursor.lines), func(index int) bool { return cursor.lines[index].End >= cursor.offset })
|
||||
}
|
||||
|
||||
func (cursor *Cursor) Current(unit Unit) Position {
|
||||
lineIndex := cursor.line_index()
|
||||
line := cursor.lines[lineIndex]
|
||||
position := Position{Offset: cursor.offset, Line: cursor.lineTexts[lineIndex], LineStart: line.Start, Cursor: cursor.offset - line.Start}
|
||||
switch unit {
|
||||
case Character, LineStart, LineEnd:
|
||||
if cursor.offset < len(cursor.text) {
|
||||
position.Text = string(cursor.text[cursor.offset])
|
||||
}
|
||||
case WordUnit:
|
||||
if word, ok := cursor.result.WordAt(cursor.offset); ok {
|
||||
position.Text = word.Text
|
||||
}
|
||||
default:
|
||||
position.Text = position.Line
|
||||
}
|
||||
return position
|
||||
}
|
||||
|
||||
func (cursor *Cursor) Move(unit Unit, step int) Position {
|
||||
previous := cursor.offset
|
||||
lineIndex := cursor.line_index()
|
||||
line := cursor.lines[lineIndex]
|
||||
switch unit {
|
||||
case Character:
|
||||
cursor.offset = max(0, min(len(cursor.text), cursor.offset+step))
|
||||
case WordUnit:
|
||||
if step > 0 {
|
||||
index := sort.Search(len(cursor.result.Words), func(index int) bool { return cursor.result.Words[index].Start > cursor.offset })
|
||||
if index < len(cursor.result.Words) {
|
||||
cursor.offset = cursor.result.Words[index].Start
|
||||
}
|
||||
} else if step < 0 {
|
||||
index := sort.Search(len(cursor.result.Words), func(index int) bool { return cursor.result.Words[index].Start >= cursor.offset }) - 1
|
||||
if index >= 0 {
|
||||
cursor.offset = cursor.result.Words[index].Start
|
||||
}
|
||||
}
|
||||
case LineUnit, PageUnit:
|
||||
if unit == PageUnit {
|
||||
step *= 20
|
||||
}
|
||||
next := max(0, min(len(cursor.lines)-1, lineIndex+step))
|
||||
if next != lineIndex {
|
||||
cursor.offset = cursor.lines[next].Start + min(cursor.column, cursor.lines[next].End-cursor.lines[next].Start)
|
||||
}
|
||||
case LineStart:
|
||||
cursor.offset = line.Start
|
||||
case LineEnd:
|
||||
cursor.offset = line.End
|
||||
case DocumentStart:
|
||||
cursor.offset = 0
|
||||
case DocumentEnd:
|
||||
cursor.offset = len(cursor.text)
|
||||
}
|
||||
if unit != LineUnit && unit != PageUnit {
|
||||
cursor.column = cursor.offset - cursor.lines[cursor.line_index()].Start
|
||||
}
|
||||
position := cursor.Current(unit)
|
||||
position.Moved = previous != cursor.offset
|
||||
return position
|
||||
}
|
||||
|
||||
// Set moves only the virtual cursor. It rejects offsets outside the recognized
|
||||
// text so a stale routing request cannot produce an invalid position.
|
||||
func (cursor *Cursor) Set(offset int) (Position, bool) {
|
||||
if offset < 0 || offset > len(cursor.text) {
|
||||
return Position{}, false
|
||||
}
|
||||
cursor.offset = offset
|
||||
cursor.column = offset - cursor.lines[cursor.line_index()].Start
|
||||
return cursor.Current(Character), true
|
||||
}
|
||||
|
||||
func (cursor *Cursor) Length() int { return len(cursor.text) }
|
||||
func (cursor *Cursor) Offset() int { return cursor.offset }
|
||||
func (cursor *Cursor) Text() string { return cursor.result.Text }
|
||||
func (cursor *Cursor) CurrentWord() (Word, bool) { return cursor.result.WordAt(cursor.offset) }
|
||||
|
||||
func (cursor *Cursor) Slice(start, end int) (string, bool) {
|
||||
if start < 0 || end < start || end > len(cursor.text) {
|
||||
return "", false
|
||||
}
|
||||
return string(cursor.text[start:end]), true
|
||||
}
|
||||
|
||||
// LinesFrom returns whole lines after the virtual cursor. The first line
|
||||
// begins at the cursor position; following lines retain recognition order.
|
||||
func (cursor *Cursor) LinesFrom() []ReadLine {
|
||||
index := cursor.line_index()
|
||||
first := index
|
||||
parts := make([]ReadLine, 0, len(cursor.lines)-index)
|
||||
for ; index < len(cursor.lines); index++ {
|
||||
line := cursor.lines[index]
|
||||
start := line.Start
|
||||
if index == first {
|
||||
start = cursor.offset
|
||||
}
|
||||
if start < line.End {
|
||||
parts = append(parts, ReadLine{Offset: start, Text: string(cursor.text[start:line.End])})
|
||||
}
|
||||
}
|
||||
return parts
|
||||
}
|
||||
Reference in new issue
Block a user