1
0
mirror of https://github.com/mum4k/termdash.git synced 2025-04-28 13:48:51 +08:00

197 lines
5.4 KiB
Go
Raw Normal View History

2018-05-14 22:44:18 +01:00
// Copyright 2018 Google Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package wrap implements line wrapping at character or word boundaries.
package wrap
import (
"github.com/mum4k/termdash/internal/canvas/buffer"
"github.com/mum4k/termdash/internal/runewidth"
)
// Mode sets the wrapping mode.
type Mode int
// String implements fmt.Stringer()
func (m Mode) String() string {
if n, ok := modeNames[m]; ok {
return n
}
return "ModeUnknown"
}
// modeNames maps Mode values to human readable names.
var modeNames = map[Mode]string{
Never: "WrapModeNever",
AtRunes: "WrapModeAtRunes",
AtWords: "WrapModeAtWords",
}
const (
// Never is the default wrapping mode, which disables line wrapping.
Never Mode = iota
// AtRunes is a wrapping mode where if the width of the text crosses the
// width of the canvas, wrapping is performed at rune boundaries.
AtRunes
// AtWords is a wrapping mode where if the width of the text crosses the
// width of the canvas, wrapping is performed at word boundaries. The
// wrapping still switches back to the AtRunes mode for any words that are
// longer than the width.
AtWords
)
// needed returns true if wrapping is needed for the rune at the horizontal
// position on the canvas that has the specified width.
// This will always return false if no options are provided, since the default
// behavior is to not wrap the text.
func needed(r rune, posX, width int, m Mode) bool {
rw := runewidth.RuneWidth(r)
return posX > width-rw && m == AtRunes
}
// Cells returns the cells wrapped into individual lines according to the
// specified width and wrapping mode.
//
// This function consumes any cells that contain newline characters and uses
// them to start new lines.
//
// If the mode is AtWords, this function also drops cells with leading space
// character before a word at which the wrap occurs.
func Cells(cells []*buffer.Cell, width int, m Mode) [][]*buffer.Cell {
if width <= 0 || len(cells) == 0 {
return nil
}
cs := newCellScanner(cells, width, m)
for state := scanCellLine; state != nil; state = state(cs) {
}
return cs.lines
}
// cellScannerState is a state in the FSM that scans the input text and identifies
// newlines.
type cellScannerState func(*cellScanner) cellScannerState
// cellScanner tracks the progress of scanning the input cells when finding
// lines.
type cellScanner struct {
// cells are the cells being scanned.
cells []*buffer.Cell
// nextIdx is the index of the cell that will be returned by next.
nextIdx int
// width is the width of the canvas the text will be drawn on.
width int
// posX tracks the horizontal position of the current cell on the canvas.
posX int
// mode is the wrapping mode.
mode Mode
// lines are the identified lines.
lines [][]*buffer.Cell
// line is the current line.
line []*buffer.Cell
}
// newCellScanner returns a scanner of the provided cells.
func newCellScanner(cells []*buffer.Cell, width int, m Mode) *cellScanner {
return &cellScanner{
cells: cells,
width: width,
mode: m,
}
}
// next returns the next cell and advances the scanner.
// Returns nil when there are no more cells to scan.
func (cs *cellScanner) next() *buffer.Cell {
c := cs.peek()
if c != nil {
cs.nextIdx++
}
return c
}
// peek returns the next cell without advancing the scanner's position.
// Returns nil when there are no more cells to peek at.
func (cs *cellScanner) peek() *buffer.Cell {
if cs.nextIdx >= len(cs.cells) {
return nil
}
return cs.cells[cs.nextIdx]
}
// peekPrev returns the previous cell without changing the scanner's position.
// Returns nil if the scanner is at the first cell.
func (cs *cellScanner) peekPrev() *buffer.Cell {
if cs.nextIdx == 0 {
return nil
}
return cs.cells[cs.nextIdx-1]
}
// scanCellLine scans a line until it finds its end due to a newline character
// or the specified width.
func scanCellLine(cs *cellScanner) cellScannerState {
for {
cell := cs.next()
if cell == nil {
if len(cs.line) > 0 || cs.peekPrev().Rune == '\n' {
cs.lines = append(cs.lines, cs.line)
}
return nil
}
switch r := cell.Rune; {
case r == '\n':
return scanCellLineBreak
case needed(r, cs.posX, cs.width, cs.mode):
return scanCellLineWrap
default:
// Move horizontally within the line for each scanned cell.
cs.posX += runewidth.RuneWidth(r)
// Copy the cell into the current line.
cs.line = append(cs.line, cell)
}
}
}
// scanCellLineBreak processes a newline character cell.
func scanCellLineBreak(cs *cellScanner) cellScannerState {
cs.lines = append(cs.lines, cs.line)
cs.posX = 0
cs.line = nil
return scanCellLine
}
// scanCellLineWrap processes a line wrap due to canvas width.
func scanCellLineWrap(cs *cellScanner) cellScannerState {
// The character on which we wrapped will be printed and is the start of
// new line.
cs.lines = append(cs.lines, cs.line)
cs.posX = runewidth.RuneWidth(cs.peekPrev().Rune)
cs.line = []*buffer.Cell{cs.peekPrev()}
return scanCellLine
}