Source: AlphaTab/ConvertToTex.js

/**
 * @class
 * @classdesc Encapsulates functions to transform internal AlphaTexStructure back into AlphaTex
 */
class ConvertToTex {
    /**
     * Converts provided AlphaTexStructure into AlphaTex
     * @param {AlphaTexStructure} alphaTexStructure The AlphaTexStructure to be converted
     * @param {object} lyrics Lyrics per track. Expected mapping track index number to lyric string
     * @returns {string} The generated AlphaTex
     */
    visitAlphaTexStructure(alphaTexStructure, lyrics) {
        let tex = "";
        alphaTexStructure.attributesStr.forEach((value, key, map) => {
            tex += "\\" + key + " " + value + "\n";
        });

        alphaTexStructure.attributesInt.forEach((value, key, map) => {
            tex += "\\" + key + " " + value + "\n";
        });

        tex += ".\n";

        let lyricIndex = 0;
        alphaTexStructure.tracks.forEach((track) => {
            tex += this.visitTrack(track, lyrics[lyricIndex]);
            lyricIndex++;
        });

        return tex;
    }

    /**
     * Converts the provided Track to AlphaTex
     * @param {Track} track The Track to be converted
     * @param {object} lyrics Lyrics per track. Expected mapping track index number to lyric string
     * @returns {string} The generated AlphaTex
     */
    visitTrack(track, lyrics) {
        let tex = "\\track " + track.getTrackName() + "\n";
        tex += this.visitTrackData(track.trackData, lyrics);
        return tex;
    }

    /**
     * Converts the provided TrackData to AlphaTex
     * @param {TrackMetaData} trackData The TrackMetaData to be converted
     * @param {object} lyrics Lyrics per track. Expected mapping track index number to lyric string
     * @returns {string} The generated AlphaTex
     */
    visitTrackData(trackData, lyrics) {
        let tex = "";
        trackData.staffs.forEach((staff) => {
            tex += this.visitStaff(staff);
        });

        return tex;
    }

    /**
     * Converts the provided Staff to AlphaTex
     * @param {Staff} staff The Staff to be converted
     * @param {object} lyrics Lyrics per track. Expected mapping track index number to lyric string
     * @returns {string} The generated AlphaTex
     */
    visitStaff(staff, lyrics) {
        let tex = "\\staff ";
        tex += "{" + staff.staffOption + "}";
        tex += this.attributesStrTex(staff.attributesStr) + "\n";
        if (lyrics) {
            tex += "\\lyrics " + lyrics + "\n"; 
        }
        staff.measures.forEach((measure) => {
            tex += this.visitMeasure(measure);
        });

        return tex;
    }

    /**
     * Converts the provided attributes String mapping to AlphaTex
     * @param {Map} attributesStr The attribute String mapping to convert. Map String attributes to String values
     * @returns {string} The generated AlphaTex
     */
    attributesStrTex(attributesStr) {
        let tex = "";
        attributesStr.forEach((value, key, map) => {
            if (tex.length > 0) {
                tex += " ";
            }
            if (key === "keySignature") {
                tex += "\\ks " + value;
            } else {
                tex += "\\" + key + " " + value;
            }
        });
        return tex;
    }

    /**
     * Converts the provided Measure to AlphaTex
     * @param {Measure} measure The Measure to be converted
     * @returns {string} The generated AlphaTex
     */
    visitMeasure(measure) {
        let tex = "";
        tex += this.attributesStrTex(measure.attributesStr);

        let ts = [-1, -1];
        measure.attributesInt.forEach((value, key, map) => {
            if (key == "tsTop") {
                ts[0] = value;
                if (ts[1] != -1) {
                    tex += " \\ts " + ts[0] + " " + ts[1];
                    ts[0] = -1;
                    ts[1] = -1;
                }
            } else if (key == "tsBottom") {
                ts[1] = value;
                if (ts[0] != -1) {
                    tex += " \\ts " + ts[0] + " " + ts[1];
                    ts[0] = -1;
                    ts[1] = -1;
                }
            } else {
                tex += " \\" + key + " " + value;
            }
        });

        measure.chords.forEach((chord) => {
            if (tex.length > 0) {
                tex += " ";
            }
            tex += this.visitChord(chord);
        });

        tex += " |\n";

        return tex;
    }

    /**
     * Converts the provided Map to a space separated list of key and value pairs
     * @param {Map} effects Map to be converted
     * @returns {string} The space separated list of key and value pairs
     */
    getEffectText(effects) {
        let effectText = "";
        effects.forEach((value, key, map) => {
            if (effectText.length > 0) {
                effectText += " ";
            }
            if (value === true) {
                effectText += key;
            } else {
                effectText += key + " " + value;
            }
        });
        return effectText;
    }

    /**
     * Converts the provided Chord to AlphaTex
     * @param {Chord} chord The Chord to be converted
     * @returns {string} The generated AlphaTex
     */
    visitChord(chord) {
        let tex = "";
        if (chord.notes.length == 1) {
            tex += this.visitNote(chord.notes[0], false);
        } else {
            tex += "(";
            let duration = -1;
            let effects = new Map();
            let noteText = "";
            chord.notes.forEach((note) => {
                let noteObj = this.visitNote(note, true);
                duration = noteObj.duration;
                noteObj.effects.after.forEach((value, key, map) => {
                    effects.set(key, value);
                });
                if (noteText.length > 0) {
                    noteText += " ";
                }
                noteText += noteObj.tex;
                let effectTextBefore = this.getEffectText(noteObj.effects.before);
                if (effectTextBefore.length > 0) {
                    noteText += "{" + effectTextBefore + "}";
                }
            });
            tex += noteText + ")";
            if (duration !== -1) {
                tex += "." + duration;
            }
            
            let effectText = this.getEffectText(effects);
            if (effectText.length > 0) {
                tex += "{" + effectText + "}";
            }
        }
        return tex;
    }

    /**
     * Converts the provided effect back to its AlphaTex version
     * @param {string} effect The effect String to be converted (expects one of: tied, dotted, crescendo, decrescendo)
     * @returns {string} The AlphaTex version of the effect
     */
    effectToSymbol(effect) {
        if (effect === "tied") {
            return "-";
        } else if (effect === "dotted") {
            return "d";
        } else if (effect === "crescendo") {
            return "cre";
        } else if (effect === "decrescendo") {
            return "dec";
        } else {
            console.log("ERROR- what:", effect);
            return effect;
        }
    }

    /**
     * @typedef {Object} NotePackage
     * @property {string} tex The generated AlphaTex
     * @property {number} duration The duration of the note
     * @property {object} effects
     * @property {Map} effects.effectsBefore The effects needed to be placed before the "." for the note
     * @property {Map} effects.effectsAfter The effects needed to be placed after the duration for the note
     */

    /**
     * Converts the provided Note to AlphaTex
     * @param {Note} note The Note to be translated
     * @param {boolean} splitInfo Boolean set to true if the translated information should be returned as an object (split into pieces)
     * @returns {(NotePackage|string)} If splitInfo is true, returns a NotePackage. Else, returns the generated AlphaTex
     */
    visitNote(note, splitInfo) {
        let tex = note.note;
        if (note.octave > 0) {
            tex += note.octave;
        }

        let effectsBefore = new Map();
        let effectsAfter = new Map();
        if (note.beatEffects) {
            note.beatEffects.forEach((value1, value2, set) => {
                let rel = value1.split(" ");
                if (Array.isArray(rel)) {
                    if (rel[0] === "tuplet") {
                        effectsAfter.set("tu", rel[1]);
                    } else if (rel[0] === "dynamic") {
                        effectsAfter.set("dy", rel[1]);
                    } else {
                        let symbol = this.effectToSymbol(rel[0]);
                        if (note.octave > 0) {
                            if (symbol === "-" || symbol === "d") {
                                effectsBefore.set(symbol, true);
                            } else {
                                effectsAfter.set(symbol, true);
                            }
                        } else {
                            effectsAfter.set(symbol, true);
                        }
                    }
                } else {
                    let symbol = this.effectToSymbol(rel);
                    if (note.octave > 0) {
                        if (symbol === "-" || symbol === "d") {
                            effectsBefore.set(symbol, true);
                        } else {
                            effectsAfter.set(symbol, true);
                        }
                    } else {
                        effectsAfter.set(symbol, true);
                    }
                }
            });
        }

        if (splitInfo) {
            let retObj = {
                tex,
                duration: note.duration,
                effects: {
                    before: effectsBefore,
                    after: effectsAfter
                }
            }
            return retObj;
        } else {
            let effectTextBefore = this.getEffectText(effectsBefore);
            if (effectTextBefore.length > 0) {
                tex += "{" + effectTextBefore + "}";
            }
            tex += "." + note.duration;
            let effectTextAfter = this.getEffectText(effectsAfter);
            if (effectTextAfter.length > 0) {
                tex += "{" + effectTextAfter + "}";
            }
            return tex;
        }
    }
}

module.exports = ConvertToTex;