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.
|
|
|
|
|
2019-02-24 23:10:52 -05:00
|
|
|
// Package wrap implements line wrapping at character or word boundaries.
|
|
|
|
package wrap
|
2018-05-14 22:12:08 +01:00
|
|
|
|
|
|
|
import (
|
2019-02-28 00:50:16 -05:00
|
|
|
"github.com/mum4k/termdash/internal/canvas/buffer"
|
2019-02-24 01:27:17 -05:00
|
|
|
"github.com/mum4k/termdash/internal/runewidth"
|
2018-05-14 22:12:08 +01:00
|
|
|
)
|
|
|
|
|
2019-02-24 23:10:52 -05:00
|
|
|
// 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.
|
2019-02-25 00:33:27 -05:00
|
|
|
var modeNames = map[Mode]string{
|
|
|
|
Never: "WrapModeNever",
|
|
|
|
AtRunes: "WrapModeAtRunes",
|
|
|
|
AtWords: "WrapModeAtWords",
|
|
|
|
}
|
2019-02-24 23:10:52 -05:00
|
|
|
|
|
|
|
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
|
2019-02-28 00:50:16 -05:00
|
|
|
// 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.
|
2019-02-24 23:10:52 -05:00
|
|
|
AtWords
|
|
|
|
)
|
|
|
|
|
2019-02-25 00:33:27 -05:00
|
|
|
// needed returns true if wrapping is needed for the rune at the horizontal
|
2019-02-24 23:10:52 -05:00
|
|
|
// 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.
|
2019-02-25 00:33:27 -05:00
|
|
|
func needed(r rune, posX, width int, m Mode) bool {
|
2018-05-20 22:51:38 +01:00
|
|
|
rw := runewidth.RuneWidth(r)
|
2019-02-24 23:10:52 -05:00
|
|
|
return posX > width-rw && m == AtRunes
|
2018-05-14 22:12:08 +01:00
|
|
|
}
|
|
|
|
|
2019-02-28 00:50:16 -05:00
|
|
|
// 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 {
|
2018-05-14 22:12:08 +01:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2019-02-28 00:50:16 -05:00
|
|
|
cs := newCellScanner(cells, width, m)
|
|
|
|
for state := scanCellLine; state != nil; state = state(cs) {
|
2018-05-14 22:12:08 +01:00
|
|
|
}
|
2019-02-28 00:50:16 -05:00
|
|
|
return cs.lines
|
2018-05-14 22:12:08 +01:00
|
|
|
}
|
|
|
|
|
2019-02-28 00:50:16 -05:00
|
|
|
// 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
|
2018-05-14 22:12:08 +01:00
|
|
|
|
2019-02-24 23:10:52 -05:00
|
|
|
// width is the width of the canvas the text will be drawn on.
|
|
|
|
width int
|
2018-05-14 22:12:08 +01:00
|
|
|
|
2019-02-28 00:50:16 -05:00
|
|
|
// posX tracks the horizontal position of the current cell on the canvas.
|
2019-02-24 23:10:52 -05:00
|
|
|
posX int
|
2018-05-14 22:12:08 +01:00
|
|
|
|
2019-02-24 23:10:52 -05:00
|
|
|
// mode is the wrapping mode.
|
|
|
|
mode Mode
|
2018-05-14 22:12:08 +01:00
|
|
|
|
2019-02-28 00:50:16 -05:00
|
|
|
// lines are the identified lines.
|
|
|
|
lines [][]*buffer.Cell
|
|
|
|
|
|
|
|
// line is the current line.
|
|
|
|
line []*buffer.Cell
|
2018-05-14 22:12:08 +01:00
|
|
|
}
|
|
|
|
|
2019-02-28 00:50:16 -05:00
|
|
|
// 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,
|
2018-05-14 22:12:08 +01:00
|
|
|
}
|
2019-02-28 00:50:16 -05:00
|
|
|
}
|
2018-05-14 22:12:08 +01:00
|
|
|
|
2019-02-28 00:50:16 -05:00
|
|
|
// 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++
|
2018-05-14 22:12:08 +01:00
|
|
|
}
|
2019-02-28 00:50:16 -05:00
|
|
|
return c
|
2018-05-14 22:12:08 +01:00
|
|
|
}
|
|
|
|
|
2019-02-28 00:50:16 -05:00
|
|
|
// 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) {
|
2018-05-14 22:12:08 +01:00
|
|
|
return nil
|
2019-02-28 00:50:16 -05:00
|
|
|
}
|
|
|
|
return cs.cells[cs.nextIdx]
|
|
|
|
}
|
2018-05-14 22:12:08 +01:00
|
|
|
|
2019-02-28 00:50:16 -05:00
|
|
|
// 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
|
2018-05-14 22:12:08 +01:00
|
|
|
}
|
2019-02-28 00:50:16 -05:00
|
|
|
return cs.cells[cs.nextIdx-1]
|
2018-05-14 22:12:08 +01:00
|
|
|
}
|
|
|
|
|
2019-02-28 00:50:16 -05:00
|
|
|
// scanCellLine scans a line until it finds its end due to a newline character
|
|
|
|
// or the specified width.
|
|
|
|
func scanCellLine(cs *cellScanner) cellScannerState {
|
2018-05-14 22:12:08 +01:00
|
|
|
for {
|
2019-02-28 00:50:16 -05:00
|
|
|
|
|
|
|
cell := cs.next()
|
|
|
|
if cell == nil {
|
|
|
|
if len(cs.line) > 0 || cs.peekPrev().Rune == '\n' {
|
|
|
|
cs.lines = append(cs.lines, cs.line)
|
|
|
|
}
|
2018-05-14 22:12:08 +01:00
|
|
|
return nil
|
2019-02-28 00:50:16 -05:00
|
|
|
}
|
2018-05-14 22:12:08 +01:00
|
|
|
|
2019-02-28 00:50:16 -05:00
|
|
|
switch r := cell.Rune; {
|
|
|
|
case r == '\n':
|
|
|
|
return scanCellLineBreak
|
2018-05-14 22:12:08 +01:00
|
|
|
|
2019-02-28 00:50:16 -05:00
|
|
|
case needed(r, cs.posX, cs.width, cs.mode):
|
|
|
|
return scanCellLineWrap
|
2018-05-14 22:12:08 +01:00
|
|
|
|
|
|
|
default:
|
2019-02-28 00:50:16 -05:00
|
|
|
// 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)
|
2018-05-14 22:12:08 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-02-28 00:50:16 -05:00
|
|
|
// 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
|
2018-05-14 22:12:08 +01:00
|
|
|
}
|
|
|
|
|
2019-02-28 00:50:16 -05:00
|
|
|
// scanCellLineWrap processes a line wrap due to canvas width.
|
|
|
|
func scanCellLineWrap(cs *cellScanner) cellScannerState {
|
2018-05-14 22:12:08 +01:00
|
|
|
// The character on which we wrapped will be printed and is the start of
|
|
|
|
// new line.
|
2019-02-28 00:50:16 -05:00
|
|
|
cs.lines = append(cs.lines, cs.line)
|
|
|
|
cs.posX = runewidth.RuneWidth(cs.peekPrev().Rune)
|
|
|
|
cs.line = []*buffer.Cell{cs.peekPrev()}
|
|
|
|
return scanCellLine
|
2018-05-14 22:12:08 +01:00
|
|
|
}
|