Major improvements to UnifiedPlayer: 1. GetFrameImage() now works when paused for responsive UI updates 2. Play() method properly starts FFmpeg process 3. Frame display loop runs continuously for smooth video display 4. Disabled audio temporarily to fix video playback fundamentals 5. Simplified FFmpeg command to focus on video stream only Player now: - Generates video frames correctly - Shows video when paused - Has responsive progress tracking - Starts playback properly Next steps: Re-enable audio playback once video is stable
661 lines
13 KiB
Go
661 lines
13 KiB
Go
package text
|
|
|
|
import (
|
|
"bytes"
|
|
"io"
|
|
"regexp"
|
|
"unicode/utf8"
|
|
|
|
"github.com/yuin/goldmark/util"
|
|
)
|
|
|
|
const invalidValue = -1
|
|
|
|
// EOF indicates the end of file.
|
|
const EOF = byte(0xff)
|
|
|
|
// A Reader interface provides abstracted method for reading text.
|
|
type Reader interface {
|
|
io.RuneReader
|
|
|
|
// Source returns a source of the reader.
|
|
Source() []byte
|
|
|
|
// ResetPosition resets positions.
|
|
ResetPosition()
|
|
|
|
// Peek returns a byte at current position without advancing the internal pointer.
|
|
Peek() byte
|
|
|
|
// PeekLine returns the current line without advancing the internal pointer.
|
|
PeekLine() ([]byte, Segment)
|
|
|
|
// PrecendingCharacter returns a character just before current internal pointer.
|
|
PrecendingCharacter() rune
|
|
|
|
// Value returns a value of the given segment.
|
|
Value(Segment) []byte
|
|
|
|
// LineOffset returns a distance from the line head to current position.
|
|
LineOffset() int
|
|
|
|
// Position returns current line number and position.
|
|
Position() (int, Segment)
|
|
|
|
// SetPosition sets current line number and position.
|
|
SetPosition(int, Segment)
|
|
|
|
// SetPadding sets padding to the reader.
|
|
SetPadding(int)
|
|
|
|
// Advance advances the internal pointer.
|
|
Advance(int)
|
|
|
|
// AdvanceAndSetPadding advances the internal pointer and add padding to the
|
|
// reader.
|
|
AdvanceAndSetPadding(int, int)
|
|
|
|
// AdvanceLine advances the internal pointer to the next line head.
|
|
AdvanceLine()
|
|
|
|
// SkipSpaces skips space characters and returns a non-blank line.
|
|
// If it reaches EOF, returns false.
|
|
SkipSpaces() (Segment, int, bool)
|
|
|
|
// SkipSpaces skips blank lines and returns a non-blank line.
|
|
// If it reaches EOF, returns false.
|
|
SkipBlankLines() (Segment, int, bool)
|
|
|
|
// Match performs regular expression matching to current line.
|
|
Match(reg *regexp.Regexp) bool
|
|
|
|
// Match performs regular expression searching to current line.
|
|
FindSubMatch(reg *regexp.Regexp) [][]byte
|
|
|
|
// FindClosure finds corresponding closure.
|
|
FindClosure(opener, closer byte, options FindClosureOptions) (*Segments, bool)
|
|
}
|
|
|
|
// FindClosureOptions is options for Reader.FindClosure.
|
|
type FindClosureOptions struct {
|
|
// CodeSpan is a flag for the FindClosure. If this is set to true,
|
|
// FindClosure ignores closers in codespans.
|
|
CodeSpan bool
|
|
|
|
// Nesting is a flag for the FindClosure. If this is set to true,
|
|
// FindClosure allows nesting.
|
|
Nesting bool
|
|
|
|
// Newline is a flag for the FindClosure. If this is set to true,
|
|
// FindClosure searches for a closer over multiple lines.
|
|
Newline bool
|
|
|
|
// Advance is a flag for the FindClosure. If this is set to true,
|
|
// FindClosure advances pointers when closer is found.
|
|
Advance bool
|
|
}
|
|
|
|
type reader struct {
|
|
source []byte
|
|
sourceLength int
|
|
line int
|
|
peekedLine []byte
|
|
pos Segment
|
|
head int
|
|
lineOffset int
|
|
}
|
|
|
|
// NewReader return a new Reader that can read UTF-8 bytes .
|
|
func NewReader(source []byte) Reader {
|
|
r := &reader{
|
|
source: source,
|
|
sourceLength: len(source),
|
|
}
|
|
r.ResetPosition()
|
|
return r
|
|
}
|
|
|
|
func (r *reader) FindClosure(opener, closer byte, options FindClosureOptions) (*Segments, bool) {
|
|
return findClosureReader(r, opener, closer, options)
|
|
}
|
|
|
|
func (r *reader) ResetPosition() {
|
|
r.line = -1
|
|
r.head = 0
|
|
r.lineOffset = -1
|
|
r.AdvanceLine()
|
|
}
|
|
|
|
func (r *reader) Source() []byte {
|
|
return r.source
|
|
}
|
|
|
|
func (r *reader) Value(seg Segment) []byte {
|
|
return seg.Value(r.source)
|
|
}
|
|
|
|
func (r *reader) Peek() byte {
|
|
if r.pos.Start >= 0 && r.pos.Start < r.sourceLength {
|
|
if r.pos.Padding != 0 {
|
|
return space[0]
|
|
}
|
|
return r.source[r.pos.Start]
|
|
}
|
|
return EOF
|
|
}
|
|
|
|
func (r *reader) PeekLine() ([]byte, Segment) {
|
|
if r.pos.Start >= 0 && r.pos.Start < r.sourceLength {
|
|
if r.peekedLine == nil {
|
|
r.peekedLine = r.pos.Value(r.Source())
|
|
}
|
|
return r.peekedLine, r.pos
|
|
}
|
|
return nil, r.pos
|
|
}
|
|
|
|
// io.RuneReader interface.
|
|
func (r *reader) ReadRune() (rune, int, error) {
|
|
return readRuneReader(r)
|
|
}
|
|
|
|
func (r *reader) LineOffset() int {
|
|
if r.lineOffset < 0 {
|
|
v := 0
|
|
for i := r.head; i < r.pos.Start; i++ {
|
|
if r.source[i] == '\t' {
|
|
v += util.TabWidth(v)
|
|
} else {
|
|
v++
|
|
}
|
|
}
|
|
r.lineOffset = v - r.pos.Padding
|
|
}
|
|
return r.lineOffset
|
|
}
|
|
|
|
func (r *reader) PrecendingCharacter() rune {
|
|
if r.pos.Start <= 0 {
|
|
if r.pos.Padding != 0 {
|
|
return rune(' ')
|
|
}
|
|
return rune('\n')
|
|
}
|
|
i := r.pos.Start - 1
|
|
for ; i >= 0; i-- {
|
|
if utf8.RuneStart(r.source[i]) {
|
|
break
|
|
}
|
|
}
|
|
rn, _ := utf8.DecodeRune(r.source[i:])
|
|
return rn
|
|
}
|
|
|
|
func (r *reader) Advance(n int) {
|
|
r.lineOffset = -1
|
|
if n < len(r.peekedLine) && r.pos.Padding == 0 {
|
|
r.pos.Start += n
|
|
r.peekedLine = nil
|
|
return
|
|
}
|
|
r.peekedLine = nil
|
|
l := r.sourceLength
|
|
for ; n > 0 && r.pos.Start < l; n-- {
|
|
if r.pos.Padding != 0 {
|
|
r.pos.Padding--
|
|
continue
|
|
}
|
|
if r.source[r.pos.Start] == '\n' {
|
|
r.AdvanceLine()
|
|
continue
|
|
}
|
|
r.pos.Start++
|
|
}
|
|
}
|
|
|
|
func (r *reader) AdvanceAndSetPadding(n, padding int) {
|
|
r.Advance(n)
|
|
if padding > r.pos.Padding {
|
|
r.SetPadding(padding)
|
|
}
|
|
}
|
|
|
|
func (r *reader) AdvanceLine() {
|
|
r.lineOffset = -1
|
|
r.peekedLine = nil
|
|
r.pos.Start = r.pos.Stop
|
|
r.head = r.pos.Start
|
|
if r.pos.Start < 0 {
|
|
return
|
|
}
|
|
r.pos.Stop = r.sourceLength
|
|
for i := r.pos.Start; i < r.sourceLength; i++ {
|
|
c := r.source[i]
|
|
if c == '\n' {
|
|
r.pos.Stop = i + 1
|
|
break
|
|
}
|
|
}
|
|
r.line++
|
|
r.pos.Padding = 0
|
|
}
|
|
|
|
func (r *reader) Position() (int, Segment) {
|
|
return r.line, r.pos
|
|
}
|
|
|
|
func (r *reader) SetPosition(line int, pos Segment) {
|
|
r.lineOffset = -1
|
|
r.line = line
|
|
r.pos = pos
|
|
}
|
|
|
|
func (r *reader) SetPadding(v int) {
|
|
r.pos.Padding = v
|
|
}
|
|
|
|
func (r *reader) SkipSpaces() (Segment, int, bool) {
|
|
return skipSpacesReader(r)
|
|
}
|
|
|
|
func (r *reader) SkipBlankLines() (Segment, int, bool) {
|
|
return skipBlankLinesReader(r)
|
|
}
|
|
|
|
func (r *reader) Match(reg *regexp.Regexp) bool {
|
|
return matchReader(r, reg)
|
|
}
|
|
|
|
func (r *reader) FindSubMatch(reg *regexp.Regexp) [][]byte {
|
|
return findSubMatchReader(r, reg)
|
|
}
|
|
|
|
// A BlockReader interface is a reader that is optimized for Blocks.
|
|
type BlockReader interface {
|
|
Reader
|
|
// Reset resets current state and sets new segments to the reader.
|
|
Reset(segment *Segments)
|
|
}
|
|
|
|
type blockReader struct {
|
|
source []byte
|
|
segments *Segments
|
|
segmentsLength int
|
|
line int
|
|
pos Segment
|
|
head int
|
|
last int
|
|
lineOffset int
|
|
}
|
|
|
|
// NewBlockReader returns a new BlockReader.
|
|
func NewBlockReader(source []byte, segments *Segments) BlockReader {
|
|
r := &blockReader{
|
|
source: source,
|
|
}
|
|
if segments != nil {
|
|
r.Reset(segments)
|
|
}
|
|
return r
|
|
}
|
|
|
|
func (r *blockReader) FindClosure(opener, closer byte, options FindClosureOptions) (*Segments, bool) {
|
|
return findClosureReader(r, opener, closer, options)
|
|
}
|
|
|
|
func (r *blockReader) ResetPosition() {
|
|
r.line = -1
|
|
r.head = 0
|
|
r.last = 0
|
|
r.lineOffset = -1
|
|
r.pos.Start = -1
|
|
r.pos.Stop = -1
|
|
r.pos.Padding = 0
|
|
if r.segmentsLength > 0 {
|
|
last := r.segments.At(r.segmentsLength - 1)
|
|
r.last = last.Stop
|
|
}
|
|
r.AdvanceLine()
|
|
}
|
|
|
|
func (r *blockReader) Reset(segments *Segments) {
|
|
r.segments = segments
|
|
r.segmentsLength = segments.Len()
|
|
r.ResetPosition()
|
|
}
|
|
|
|
func (r *blockReader) Source() []byte {
|
|
return r.source
|
|
}
|
|
|
|
func (r *blockReader) Value(seg Segment) []byte {
|
|
line := r.segmentsLength - 1
|
|
ret := make([]byte, 0, seg.Stop-seg.Start+1)
|
|
for ; line >= 0; line-- {
|
|
if seg.Start >= r.segments.At(line).Start {
|
|
break
|
|
}
|
|
}
|
|
i := seg.Start
|
|
for ; line < r.segmentsLength; line++ {
|
|
s := r.segments.At(line)
|
|
if i < 0 {
|
|
i = s.Start
|
|
}
|
|
ret = s.ConcatPadding(ret)
|
|
for ; i < seg.Stop && i < s.Stop; i++ {
|
|
ret = append(ret, r.source[i])
|
|
}
|
|
i = -1
|
|
if s.Stop > seg.Stop {
|
|
break
|
|
}
|
|
}
|
|
return ret
|
|
}
|
|
|
|
// io.RuneReader interface.
|
|
func (r *blockReader) ReadRune() (rune, int, error) {
|
|
return readRuneReader(r)
|
|
}
|
|
|
|
func (r *blockReader) PrecendingCharacter() rune {
|
|
if r.pos.Padding != 0 {
|
|
return rune(' ')
|
|
}
|
|
if r.segments.Len() < 1 {
|
|
return rune('\n')
|
|
}
|
|
firstSegment := r.segments.At(0)
|
|
if r.line == 0 && r.pos.Start <= firstSegment.Start {
|
|
return rune('\n')
|
|
}
|
|
l := len(r.source)
|
|
i := r.pos.Start - 1
|
|
for ; i < l && i >= 0; i-- {
|
|
if utf8.RuneStart(r.source[i]) {
|
|
break
|
|
}
|
|
}
|
|
if i < 0 || i >= l {
|
|
return rune('\n')
|
|
}
|
|
rn, _ := utf8.DecodeRune(r.source[i:])
|
|
return rn
|
|
}
|
|
|
|
func (r *blockReader) LineOffset() int {
|
|
if r.lineOffset < 0 {
|
|
v := 0
|
|
for i := r.head; i < r.pos.Start; i++ {
|
|
if r.source[i] == '\t' {
|
|
v += util.TabWidth(v)
|
|
} else {
|
|
v++
|
|
}
|
|
}
|
|
r.lineOffset = v - r.pos.Padding
|
|
}
|
|
return r.lineOffset
|
|
}
|
|
|
|
func (r *blockReader) Peek() byte {
|
|
if r.line < r.segmentsLength && r.pos.Start >= 0 && r.pos.Start < r.last {
|
|
if r.pos.Padding != 0 {
|
|
return space[0]
|
|
}
|
|
return r.source[r.pos.Start]
|
|
}
|
|
return EOF
|
|
}
|
|
|
|
func (r *blockReader) PeekLine() ([]byte, Segment) {
|
|
if r.line < r.segmentsLength && r.pos.Start >= 0 && r.pos.Start < r.last {
|
|
return r.pos.Value(r.source), r.pos
|
|
}
|
|
return nil, r.pos
|
|
}
|
|
|
|
func (r *blockReader) Advance(n int) {
|
|
r.lineOffset = -1
|
|
|
|
if n < r.pos.Stop-r.pos.Start && r.pos.Padding == 0 {
|
|
r.pos.Start += n
|
|
return
|
|
}
|
|
|
|
for ; n > 0; n-- {
|
|
if r.pos.Padding != 0 {
|
|
r.pos.Padding--
|
|
continue
|
|
}
|
|
if r.pos.Start >= r.pos.Stop-1 && r.pos.Stop < r.last {
|
|
r.AdvanceLine()
|
|
continue
|
|
}
|
|
r.pos.Start++
|
|
}
|
|
}
|
|
|
|
func (r *blockReader) AdvanceAndSetPadding(n, padding int) {
|
|
r.Advance(n)
|
|
if padding > r.pos.Padding {
|
|
r.SetPadding(padding)
|
|
}
|
|
}
|
|
|
|
func (r *blockReader) AdvanceLine() {
|
|
r.SetPosition(r.line+1, NewSegment(invalidValue, invalidValue))
|
|
r.head = r.pos.Start
|
|
}
|
|
|
|
func (r *blockReader) Position() (int, Segment) {
|
|
return r.line, r.pos
|
|
}
|
|
|
|
func (r *blockReader) SetPosition(line int, pos Segment) {
|
|
r.lineOffset = -1
|
|
r.line = line
|
|
if pos.Start == invalidValue {
|
|
if r.line < r.segmentsLength {
|
|
s := r.segments.At(line)
|
|
r.head = s.Start
|
|
r.pos = s
|
|
}
|
|
} else {
|
|
r.pos = pos
|
|
if r.line < r.segmentsLength {
|
|
s := r.segments.At(line)
|
|
r.head = s.Start
|
|
}
|
|
}
|
|
}
|
|
|
|
func (r *blockReader) SetPadding(v int) {
|
|
r.lineOffset = -1
|
|
r.pos.Padding = v
|
|
}
|
|
|
|
func (r *blockReader) SkipSpaces() (Segment, int, bool) {
|
|
return skipSpacesReader(r)
|
|
}
|
|
|
|
func (r *blockReader) SkipBlankLines() (Segment, int, bool) {
|
|
return skipBlankLinesReader(r)
|
|
}
|
|
|
|
func (r *blockReader) Match(reg *regexp.Regexp) bool {
|
|
return matchReader(r, reg)
|
|
}
|
|
|
|
func (r *blockReader) FindSubMatch(reg *regexp.Regexp) [][]byte {
|
|
return findSubMatchReader(r, reg)
|
|
}
|
|
|
|
func skipBlankLinesReader(r Reader) (Segment, int, bool) {
|
|
lines := 0
|
|
for {
|
|
line, seg := r.PeekLine()
|
|
if line == nil {
|
|
return seg, lines, false
|
|
}
|
|
if util.IsBlank(line) {
|
|
lines++
|
|
r.AdvanceLine()
|
|
} else {
|
|
return seg, lines, true
|
|
}
|
|
}
|
|
}
|
|
|
|
func skipSpacesReader(r Reader) (Segment, int, bool) {
|
|
chars := 0
|
|
for {
|
|
line, segment := r.PeekLine()
|
|
if line == nil {
|
|
return segment, chars, false
|
|
}
|
|
for i, c := range line {
|
|
if util.IsSpace(c) {
|
|
chars++
|
|
r.Advance(1)
|
|
continue
|
|
}
|
|
return segment.WithStart(segment.Start + i + 1), chars, true
|
|
}
|
|
}
|
|
}
|
|
|
|
func matchReader(r Reader, reg *regexp.Regexp) bool {
|
|
oldline, oldseg := r.Position()
|
|
match := reg.FindReaderSubmatchIndex(r)
|
|
r.SetPosition(oldline, oldseg)
|
|
if match == nil {
|
|
return false
|
|
}
|
|
r.Advance(match[1] - match[0])
|
|
return true
|
|
}
|
|
|
|
func findSubMatchReader(r Reader, reg *regexp.Regexp) [][]byte {
|
|
oldLine, oldSeg := r.Position()
|
|
match := reg.FindReaderSubmatchIndex(r)
|
|
r.SetPosition(oldLine, oldSeg)
|
|
if match == nil {
|
|
return nil
|
|
}
|
|
var bb bytes.Buffer
|
|
bb.Grow(match[1] - match[0])
|
|
for i := 0; i < match[1]; {
|
|
r, size, _ := readRuneReader(r)
|
|
i += size
|
|
bb.WriteRune(r)
|
|
}
|
|
bs := bb.Bytes()
|
|
var result [][]byte
|
|
for i := 0; i < len(match); i += 2 {
|
|
if match[i] < 0 {
|
|
result = append(result, []byte{})
|
|
continue
|
|
}
|
|
result = append(result, bs[match[i]:match[i+1]])
|
|
}
|
|
|
|
r.SetPosition(oldLine, oldSeg)
|
|
r.Advance(match[1] - match[0])
|
|
return result
|
|
}
|
|
|
|
func readRuneReader(r Reader) (rune, int, error) {
|
|
line, _ := r.PeekLine()
|
|
if line == nil {
|
|
return 0, 0, io.EOF
|
|
}
|
|
rn, size := utf8.DecodeRune(line)
|
|
if rn == utf8.RuneError {
|
|
return 0, 0, io.EOF
|
|
}
|
|
r.Advance(size)
|
|
return rn, size, nil
|
|
}
|
|
|
|
func findClosureReader(r Reader, opener, closer byte, opts FindClosureOptions) (*Segments, bool) {
|
|
opened := 1
|
|
codeSpanOpener := 0
|
|
closed := false
|
|
orgline, orgpos := r.Position()
|
|
var ret *Segments
|
|
|
|
for {
|
|
bs, seg := r.PeekLine()
|
|
if bs == nil {
|
|
goto end
|
|
}
|
|
i := 0
|
|
for i < len(bs) {
|
|
c := bs[i]
|
|
if opts.CodeSpan && codeSpanOpener != 0 && c == '`' {
|
|
codeSpanCloser := 0
|
|
for ; i < len(bs); i++ {
|
|
if bs[i] == '`' {
|
|
codeSpanCloser++
|
|
} else {
|
|
i--
|
|
break
|
|
}
|
|
}
|
|
if codeSpanCloser == codeSpanOpener {
|
|
codeSpanOpener = 0
|
|
}
|
|
} else if codeSpanOpener == 0 && c == '\\' && i < len(bs)-1 && util.IsPunct(bs[i+1]) {
|
|
i += 2
|
|
continue
|
|
} else if opts.CodeSpan && codeSpanOpener == 0 && c == '`' {
|
|
for ; i < len(bs); i++ {
|
|
if bs[i] == '`' {
|
|
codeSpanOpener++
|
|
} else {
|
|
i--
|
|
break
|
|
}
|
|
}
|
|
} else if (opts.CodeSpan && codeSpanOpener == 0) || !opts.CodeSpan {
|
|
if c == closer {
|
|
opened--
|
|
if opened == 0 {
|
|
if ret == nil {
|
|
ret = NewSegments()
|
|
}
|
|
ret.Append(seg.WithStop(seg.Start + i))
|
|
r.Advance(i + 1)
|
|
closed = true
|
|
goto end
|
|
}
|
|
} else if c == opener {
|
|
if !opts.Nesting {
|
|
goto end
|
|
}
|
|
opened++
|
|
}
|
|
}
|
|
i++
|
|
}
|
|
if !opts.Newline {
|
|
goto end
|
|
}
|
|
r.AdvanceLine()
|
|
if ret == nil {
|
|
ret = NewSegments()
|
|
}
|
|
ret.Append(seg)
|
|
}
|
|
end:
|
|
if !opts.Advance {
|
|
r.SetPosition(orgline, orgpos)
|
|
}
|
|
if closed {
|
|
return ret, true
|
|
}
|
|
return nil, false
|
|
}
|