Files
alda-mirror/client/parser/format.go
David Lu cea51273cb Introduce NoteLengthSeconds to distinguish between 's' and 'ms' note lengths
Add NoteLengthSeconds scanner token, ASTNode, and ScoreUpdate.
NoteLengthSeconds is the seconds equivalent of NoteLengthMs.
2023-04-02 22:26:39 -04:00

756 lines
16 KiB
Go

package parser
import (
"bytes"
"fmt"
"io"
"strconv"
"strings"
)
type varDefState int
const (
None = iota // not currently in a var def
Defining // currently formatting a var def (cannot go to new line)
LastNode // last node of a var def, treated specially
)
type formatter struct {
softWrapLen int // configured line length to soft wrap formatting
indentText string // configured indent string (i.e. spaces vs tabs)
varDef varDefState // state to handle formatting variable definitions
indentLevel int // state for indentation level
texts []string // buffer of "tokens" for the ongoing formatted line
out io.Writer
}
type formatterOption func(*formatter)
func ConfigureSoftWrapLen(len int) func(*formatter) {
return func(f *formatter) {
f.softWrapLen = len
}
}
func ConfigureIndentText(text string) func(*formatter) {
return func(f *formatter) {
f.indentText = text
}
}
func newFormatter(out io.Writer, opts ...formatterOption) *formatter {
formatter := &formatter{
softWrapLen: 80,
indentText: " ",
varDef: None,
indentLevel: 0,
texts: []string{},
out: out,
}
for _, opt := range opts {
opt(formatter)
}
return formatter
}
// line constructs and returns the current line being formatted.
func (f *formatter) line() string {
text := strings.Join(f.texts, " ")
if len(text) == 0 {
return text
} else {
indent := strings.Repeat(f.indentText, f.indentLevel)
return indent + text
}
}
// emptyLine writes an empty line
func (f *formatter) emptyLine() {
if f.varDef == None {
f.flush()
f.out.Write([]byte("\n"))
}
}
// flush flushes out the current line to the output.
func (f *formatter) flush() {
if len(f.texts) > 0 && f.varDef == None {
f.out.Write([]byte(f.line() + "\n"))
f.texts = []string{}
}
}
// indent increments the indentation level of subsequent formatting.
func (f *formatter) indent() {
switch f.varDef {
case LastNode:
// During the last node of a var def, we are allowed to effectively exit
// the var def and continue formatting on the next line.
f.varDef = None
fallthrough
case None:
f.flush()
f.indentLevel++
}
}
// unindent decrements the indentation level of subsequent formatting.
// A corresponding unindent should always be called after calling indent.
func (f *formatter) unindent() {
if f.varDef == None {
f.flush()
f.indentLevel--
}
}
// write formats text to the output with indentation, wrapping, and spacing.
// Each "text" is an unwrappable token, i.e. wrapping only happens between text.
func (f *formatter) write(text string) {
f.texts = append(f.texts, text)
if len(f.line()) > f.softWrapLen && f.varDef == None {
f.texts = f.texts[0 : len(f.texts)-1]
f.flush()
f.texts = append(f.texts, text)
}
}
// formatWithDuration handles duration formatting.
// Durations are formatted with possible text directly pre/post (no spaces),
// i.e. note pitches preceding durations.
// All durations are treated as a single unwrappable text with the exception of
// barlines which cause a duration to be split into separate texts.
func (f *formatter) formatWithDuration(
pre string, duration ASTNode, post string,
) error {
text := strings.Builder{}
text.WriteString(pre)
shouldTie := false
for i, child := range duration.Children {
switch child.Type {
default:
return fmt.Errorf(
"unexpected DurationNode %#v during formatting", child,
)
case BarlineNode:
if i == len(duration.Children)-1 {
// The final duration is a barline
// We write out any post text before the barline for clarity
text.WriteString(post)
}
// Barlines in a duration split formatting into separate texts
if text.Len() > 0 {
f.write(text.String())
}
f.write("|")
text.Reset()
case NoteLengthNode:
if shouldTie {
text.WriteString("~")
}
if err := child.expectNChildren(1, 2); err != nil {
return err
}
denom, err := child.Children[0].expectNodeType(DenominatorNode)
if err != nil {
return err
}
numDots := 0
if len(child.Children) > 1 {
dotsNode, err := child.Children[1].expectNodeType(DotsNode)
if err != nil {
return err
}
numDots = int(dotsNode.Literal.(int32))
}
text.WriteString(fmt.Sprintf(
"%s%s",
strconv.FormatFloat(denom.Literal.(float64), 'f', -1, 64),
strings.Repeat(".", numDots),
))
shouldTie = true
case NoteLengthMsNode:
if shouldTie {
text.WriteString("~")
}
text.WriteString(fmt.Sprintf(
"%sms",
strconv.FormatFloat(child.Literal.(float64), 'f', -1, 64),
))
shouldTie = true
case NoteLengthSecondsNode:
if shouldTie {
text.WriteString("~")
}
text.WriteString(fmt.Sprintf(
"%ss",
strconv.FormatFloat(child.Literal.(float64), 'f', -1, 64),
))
shouldTie = true
}
}
if text.Len() > 0 {
text.WriteString(post)
f.write(text.String())
}
return nil
}
// formatInnerEvents handles formatting of inner events within parts.
func (f *formatter) formatInnerEvents(nodes ...ASTNode) error {
for _, node := range nodes {
switch node.Type {
default:
return fmt.Errorf(
"unexpected ASTNode Type %#v during formatting", node.Type,
)
case AtMarkerNode:
f.write(fmt.Sprintf("@%s", node.Literal.(string)))
case BarlineNode:
f.write("|")
case ChordNode:
// We make each note + each separator individual texts to format
// Meaning extra spaces padding separators + chords can be wrapped
// This is to avoid additional complexity in the formatter
// We can change this by creating a new helper function for
// inner-chord nodes that returns a []string of texts
// Would have to handle the fact that barlines make multiple writes
if err := node.expectChildren(); err != nil {
return err
}
// Within a chord, there can be additional nodes between notes
// We format all of these after the separator for readability as
// they apply to the subsequent note
lastNoteOrRest := 0
for i, child := range node.Children {
if child.Type == NoteNode || child.Type == RestNode {
lastNoteOrRest = i
}
}
for i, child := range node.Children {
err := f.formatInnerEvents(child)
if err != nil {
return err
}
if child.Type == NoteNode || child.Type == RestNode {
if i < lastNoteOrRest {
f.write("/")
}
}
}
case CramNode:
if err := node.expectNChildren(1, 2); err != nil {
return err
}
events, err := node.Children[0].expectNodeType(EventSequenceNode)
if err != nil {
return err
}
f.write("{")
err = f.formatInnerEvents(events.Children...)
if err != nil {
return err
}
if len(node.Children) > 1 {
duration, err := node.Children[1].expectNodeType(DurationNode)
if err != nil {
return err
}
err = f.formatWithDuration("}", duration, "")
if err != nil {
return err
}
} else {
f.write("}")
}
case EventSequenceNode:
// Always try to indent the children of standalone event sequences
// (i.e. those not used as part of a separate node such as cram)
f.flush()
f.write("[")
f.indent()
err := f.formatInnerEvents(node.Children...)
if err != nil {
return err
}
f.unindent()
f.write("]")
case LispListNode:
var lispString func(ASTNode) (string, error)
lispString = func(lisp ASTNode) (string, error) {
switch lisp.Type {
default:
return "", fmt.Errorf(
"unexpected LispListNode %#v during formatting", lisp,
)
case LispListNode:
texts := []string{}
for _, child := range lisp.Children {
text, err := lispString(child)
if err != nil {
return "", err
}
texts = append(texts, text)
}
return fmt.Sprintf("(%s)", strings.Join(texts, " ")), nil
case LispNumberNode:
switch num := lisp.Literal.(type) {
default:
return "", fmt.Errorf(
"unexpected LispNumberNode %#v during formatting",
num,
)
case float64:
return strconv.FormatFloat(
num, 'f', -1, 64,
), nil
case int32:
return fmt.Sprintf("%d", num), nil
}
case LispQuotedFormNode:
form, err := lispString(lisp.Children[0])
if err != nil {
return "", err
}
return fmt.Sprintf("'%s", form), nil
case LispStringNode:
return fmt.Sprintf("\"%s\"", lisp.Literal.(string)), nil
case LispSymbolNode:
return lisp.Literal.(string), nil
}
}
text, err := lispString(node)
if err != nil {
return err
}
// Lisp lists are generally short
// We write them as a single unwrappable text for readability
f.write(text)
case MarkerNode:
f.write(fmt.Sprintf("%%%s", node.Literal.(string)))
case NoteNode:
if err := node.expectNChildren(1, 2, 3); err != nil {
return err
}
laa, err := node.Children[0].expectNodeType(
NoteLetterAndAccidentalsNode,
)
if err != nil {
return err
}
if err := laa.expectChildren(); err != nil {
return err
}
letter, err := laa.Children[0].expectNodeType(NoteLetterNode)
if err != nil {
return err
}
pitchText := strings.Builder{}
pitchText.WriteRune(letter.Literal.(rune))
if len(laa.Children) > 1 {
accidentals, err := laa.Children[1].expectNodeType(
NoteAccidentalsNode,
)
if err != nil {
return err
}
for _, child := range accidentals.Children {
switch child.Type {
default:
return fmt.Errorf(
"unexpected NoteAccidentalsNode %#v during formatting",
child,
)
case FlatNode:
pitchText.WriteString("-")
case NaturalNode:
pitchText.WriteString("_")
case SharpNode:
pitchText.WriteString("+")
}
}
}
slurText := ""
for _, child := range node.Children[1:] {
if child.Type == TieNode {
slurText = "~"
}
}
if len(node.Children) > 1 && node.Children[1].Type == DurationNode {
err = f.formatWithDuration(
pitchText.String(), node.Children[1], slurText,
)
if err != nil {
return err
}
} else {
f.write(fmt.Sprintf("%s%s", pitchText.String(), slurText))
}
case OctaveDownNode:
f.write("<")
case OctaveSetNode:
f.write(fmt.Sprintf("o%d", node.Literal.(int32)))
case OctaveUpNode:
f.write(">")
case RepeatNode:
if err := node.expectNChildren(2); err != nil {
return err
}
err := f.formatInnerEvents(node.Children[0])
if err != nil {
return err
}
times, err := node.Children[1].expectNodeType(TimesNode)
if err != nil {
return err
}
f.write(fmt.Sprintf("*%d", times.Literal.(int32)))
case OnRepetitionsNode:
if err := node.expectNChildren(2); err != nil {
return err
}
err := f.formatInnerEvents(node.Children[0])
if err != nil {
return err
}
repetitions, err := node.Children[1].expectNodeType(RepetitionsNode)
if err != nil {
return err
}
ranges := []string{}
for _, child := range repetitions.Children {
rr, err := child.expectNodeType(RepetitionRangeNode)
if err != nil {
return err
}
if err := rr.expectNChildren(2); err != nil {
return err
}
fr, err := rr.Children[0].expectNodeType(FirstRepetitionNode)
if err != nil {
return err
}
lr, err := rr.Children[1].expectNodeType(LastRepetitionNode)
if err != nil {
return err
}
frNum := fr.Literal.(int32)
lrNum := lr.Literal.(int32)
if frNum == lrNum {
ranges = append(ranges,
fmt.Sprintf("%d", frNum),
)
} else {
ranges = append(ranges,
fmt.Sprintf("%d-%d", frNum, lrNum),
)
}
}
f.write(fmt.Sprintf("'%s", strings.Join(ranges, ",")))
case RestNode:
if len(node.Children) > 0 {
duration, err := node.Children[0].expectNodeType(DurationNode)
if err != nil {
return err
}
err = f.formatWithDuration("r", duration, "")
if err != nil {
return err
}
} else {
f.write("r")
}
case VariableDefinitionNode:
// Variable definitions are incredibly tricky to format because
// formatted text must be on the same line as the variable name.
// We handle this by maintaining varDefState:
// - While "None", behaviour is normal.
// - While "Defining", we never flush/wrap to a new line.
// - While "LastNode", we allow event sequences (including repeats)
// and voice groups to indent and continue on new lines.
// This is complicated, but any alternative I tried was worse.
f.flush()
f.varDef = Defining
if err := node.expectNChildren(2); err != nil {
return err
}
name, err := node.Children[0].expectNodeType(VariableNameNode)
if err != nil {
return err
}
f.write(fmt.Sprintf("%s =", name.Literal.(string)))
events, err := node.Children[1].expectNodeType(EventSequenceNode)
if err != nil {
return err
}
if len(events.Children) > 0 {
lastIndex := len(events.Children) - 1
err = f.formatInnerEvents(events.Children[:lastIndex]...)
if err != nil {
return err
}
f.varDef = LastNode
err = f.formatInnerEvents(events.Children[lastIndex])
if err != nil {
return err
}
}
f.varDef = None
f.flush()
case VariableReferenceNode:
f.write(node.Literal.(string))
case VoiceGroupEndMarkerNode:
f.write("V0:")
case VoiceGroupNode:
f.flush()
err := f.formatInnerEvents(node.Children...)
if err != nil {
return err
}
case VoiceNode:
if err := node.expectNChildren(2); err != nil {
return err
}
voiceNumber, err := node.Children[0].expectNodeType(VoiceNumberNode)
if err != nil {
return err
}
f.write(fmt.Sprintf("V%d:", voiceNumber.Literal.(int32)))
f.indent()
events, err := node.Children[1].expectNodeType(EventSequenceNode)
if err != nil {
return err
}
err = f.formatInnerEvents(events.Children...)
if err != nil {
return err
}
f.unindent()
}
}
return nil
}
// formatTopLevel handles formatting for the RootNode and parts.
func (f *formatter) formatTopLevel(root ASTNode) error {
for i, part := range root.Children {
switch part.Type {
case ImplicitPartNode:
if err := part.expectNChildren(1); err != nil {
return err
}
events, err := part.Children[0].expectNodeType(EventSequenceNode)
if err != nil {
return err
}
err = f.formatInnerEvents(events.Children...)
if err != nil {
return err
}
case PartNode:
if err := part.expectNChildren(2); err != nil {
return err
}
// Part declaration
decl, err := part.Children[0].expectNodeType(PartDeclarationNode)
if err != nil {
return err
}
if err := decl.expectNChildren(1, 2); err != nil {
return err
}
partNames, err := decl.Children[0].expectNodeType(PartNamesNode)
if err != nil {
return err
}
if err := partNames.expectChildren(); err != nil {
return err
}
names := []string{}
for _, child := range partNames.Children {
partNameNode, err := child.expectNodeType(PartNameNode)
if err != nil {
return err
}
names = append(names, partNameNode.Literal.(string))
}
namesText := strings.Join(names, "/")
if len(decl.Children) > 1 {
partAlias, err := decl.Children[1].expectNodeType(
PartAliasNode,
)
if err != nil {
return err
}
f.write(fmt.Sprintf(
"%s \"%s\":",
namesText,
partAlias.Literal.(string),
))
} else {
f.write(fmt.Sprintf(
"%s:",
namesText,
))
}
// Part events
f.indent()
events, err := part.Children[1].expectNodeType(EventSequenceNode)
if err != nil {
return err
}
err = f.formatInnerEvents(events.Children...)
if err != nil {
return err
}
f.unindent()
}
f.flush()
if i+1 < len(root.Children) {
f.emptyLine()
}
}
return nil
}
// FormatASTToCode performs rudimentary output formatting of Alda code including
// handling basic spacing, indentation, and line wrapping.
// TODO: handle formatting comments by retaining comment data to the AST layer.
func FormatASTToCode(
root ASTNode, out io.Writer, opts ...formatterOption,
) error {
// Write to temp buffer instead of directly to file in case of error
temp := bytes.Buffer{}
f := newFormatter(&temp, opts...)
err := f.formatTopLevel(root)
if err != nil {
return err
}
_, err = out.Write(temp.Bytes())
return err
}