@hymnbook/abc 0.0.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,52 @@
1
- // src/abcTypes.ts
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/index.ts
31
+ var index_exports = {};
32
+ __export(index_exports, {
33
+ AbcSong: () => AbcSong,
34
+ ValidationError: () => ValidationError,
35
+ addInfoFieldsToMelody: () => addInfoFieldsToMelody,
36
+ combineMelodyAndLyrics: () => combineMelodyAndLyrics,
37
+ convertStringToAbcTune: () => convertStringToAbcTune,
38
+ extractInfoFields: () => extractInfoFields,
39
+ generateAbcForVerse: () => generateAbcForVerse,
40
+ getField: () => getField,
41
+ getForVerse: () => getForVerse,
42
+ parse: () => parse,
43
+ squashNotesAndLyrics: () => squashNotesAndLyrics,
44
+ stringify: () => stringify,
45
+ validate: () => validate
46
+ });
47
+ module.exports = __toCommonJS(index_exports);
48
+
49
+ // src/types.ts
2
50
  var AbcSong = class {
3
51
  // See also: https://abcnotation.com/wiki/abc:standard:v2.1#information_fields
4
52
  area;
@@ -40,8 +88,8 @@ var AbcSong = class {
40
88
  };
41
89
  };
42
90
 
43
- // src/abc.ts
44
- import * as ABCJS from "abcjs";
91
+ // src/parser.ts
92
+ var ABCJS = __toESM(require("abcjs"), 1);
45
93
 
46
94
  // src/validation.ts
47
95
  var ValidationError = class extends Error {
@@ -53,15 +101,155 @@ function validate(result, message) {
53
101
  throw new ValidationError(message || "Value is not true");
54
102
  }
55
103
 
56
- // src/abc.ts
104
+ // src/deparser.ts
105
+ var addInfoFieldsToMelody = (song, abc) => {
106
+ let result = "";
107
+ result += song.referenceNumber === void 0 ? "" : "X:" + song.referenceNumber + "\n";
108
+ result += song.title === void 0 ? "" : "T:" + song.title + "\n";
109
+ result += song.area === void 0 ? "" : "A:" + song.area + "\n";
110
+ result += song.book === void 0 ? "" : "B:" + song.book + "\n";
111
+ result += song.composer === void 0 ? "" : "C:" + song.composer + "\n";
112
+ result += song.discography === void 0 ? "" : "D:" + song.discography + "\n";
113
+ result += song.fileUrl === void 0 ? "" : "F:" + song.fileUrl + "\n";
114
+ result += song.group === void 0 ? "" : "G:" + song.group + "\n";
115
+ result += song.history === void 0 ? "" : "H:" + song.history + "\n";
116
+ result += song.instruction === void 0 ? "" : "I:" + song.instruction + "\n";
117
+ result += song.key === void 0 ? "" : "K:" + song.key + "\n";
118
+ result += song.unitNoteLength === void 0 ? "" : "L:" + song.unitNoteLength + "\n";
119
+ result += song.meter === void 0 ? "" : "M:" + song.meter + "\n";
120
+ result += song.macro === void 0 ? "" : "m:" + song.macro + "\n";
121
+ result += song.notes === void 0 ? "" : "N:" + song.notes + "\n";
122
+ result += song.origin === void 0 ? "" : "O:" + song.origin + "\n";
123
+ result += song.parts === void 0 ? "" : "P:" + song.parts + "\n";
124
+ result += song.tempo === void 0 ? "" : "Q:" + song.tempo + "\n";
125
+ result += song.rhythm === void 0 ? "" : "R:" + song.rhythm + "\n";
126
+ result += song.remark === void 0 ? "" : "r:" + song.remark + "\n";
127
+ result += song.source === void 0 ? "" : "S:" + song.source + "\n";
128
+ result += song.symbolLine === void 0 ? "" : "s:" + song.symbolLine + "\n";
129
+ result += song.userDefined === void 0 ? "" : "U:" + song.userDefined + "\n";
130
+ result += song.voice === void 0 ? "" : "V:" + song.voice + "\n";
131
+ result += song.transcription === void 0 ? "" : "Z:" + song.transcription + "\n";
132
+ return result + abc;
133
+ };
134
+ var durationToString = (duration) => {
135
+ duration /= 4;
136
+ if (duration > 3 / 8) return (duration * 4).toString();
137
+ if (duration == 1 / 4) return "";
138
+ if (duration == 3 / 8) return "3/2";
139
+ if (duration == 1 / 8) return "/2";
140
+ if (duration == 3 / 16) return "3/4";
141
+ if (duration == 1 / 16) return "/4";
142
+ if (duration == 3 / 32) return "3/8";
143
+ if (duration == 1 / 32) return "/8";
144
+ if (duration == 3 / 64) return "3/16";
145
+ return "";
146
+ };
147
+ var voiceBarToString = (voice) => {
148
+ let result = "";
149
+ switch (voice.type) {
150
+ case "bar_thin":
151
+ result += "|";
152
+ break;
153
+ case "bar_thin_thick":
154
+ result += "|]";
155
+ break;
156
+ case "bar_thin_thin":
157
+ result += "||";
158
+ break;
159
+ case "bar_thick_thin":
160
+ result += "[|";
161
+ break;
162
+ case "bar_right_repeat":
163
+ result += ":|";
164
+ break;
165
+ case "bar_left_repeat":
166
+ result += "|:";
167
+ break;
168
+ case "bar_dbl_repeat":
169
+ result += "::";
170
+ break;
171
+ }
172
+ if (voice.startEnding) result += voice.startEnding;
173
+ return result + " ";
174
+ };
175
+ var voiceNoteToString = (voice, durationMultiplier, isBeam) => {
176
+ let notes = "";
177
+ let lyrics = "";
178
+ const duration = durationToString(voice.duration / durationMultiplier);
179
+ if (voice.startBeam) isBeam = true;
180
+ if (voice.endBeam) isBeam = false;
181
+ voice.chord?.forEach((chord) => {
182
+ const parsedChordName = chord.name.replaceAll(/♭/g, "b").replaceAll(/♯/g, "#");
183
+ notes += `"${parsedChordName}"`;
184
+ });
185
+ if (voice.pitches && voice.pitches?.length > 1) notes += "[";
186
+ voice.pitches?.forEach((pitch) => {
187
+ let note = pitch.name + duration;
188
+ if (pitch.startSlur) note = "(".repeat(pitch.startSlur.length) + note;
189
+ if (pitch.endSlur) note += ")".repeat(pitch.endSlur.length);
190
+ if (pitch.startTie) note += "-";
191
+ notes += note;
192
+ });
193
+ if (voice.pitches && voice.pitches?.length > 1) notes += "]";
194
+ if (voice.rest) {
195
+ if (voice.rest.type == "spacer") {
196
+ notes += "y";
197
+ } else if (voice.rest.type == "multimeasure") {
198
+ notes += "Z" + durationToString(voice.duration / 4);
199
+ } else {
200
+ notes += "z" + duration;
201
+ }
202
+ }
203
+ if (voice.pitches) {
204
+ voice.lyric?.forEach((lyric) => {
205
+ let text = lyric.syllable.replace(/ /g, "~");
206
+ if (text == "") text = "*";
207
+ lyrics += text + lyric.divider;
208
+ });
209
+ }
210
+ if (!isBeam) notes += " ";
211
+ return {
212
+ notes,
213
+ lyrics,
214
+ isBeam
215
+ };
216
+ };
217
+ var stringify = (song) => {
218
+ const durationParts = (song.unitNoteLength || "1/8").split("/");
219
+ const durationMultiplier = durationParts.length > 1 ? +durationParts[0] / +durationParts[1] : +durationParts[0];
220
+ const melody = song.melody.flatMap((line) => {
221
+ const notes = [];
222
+ const lyrics = [];
223
+ let isBeam = false;
224
+ line.forEach((voice) => {
225
+ switch (voice.el_type) {
226
+ case "bar":
227
+ notes.push(voiceBarToString(voice));
228
+ break;
229
+ case "note":
230
+ const result2 = voiceNoteToString(voice, durationMultiplier, isBeam);
231
+ notes.push(result2.notes);
232
+ lyrics.push(result2.lyrics);
233
+ isBeam = result2.isBeam;
234
+ break;
235
+ }
236
+ });
237
+ const result = [notes.join("").trim()];
238
+ const mergedLyrics = lyrics.join("").trim();
239
+ if (mergedLyrics.length > 0) {
240
+ result.push("w: " + mergedLyrics);
241
+ }
242
+ return result;
243
+ }).join("\n");
244
+ return addInfoFieldsToMelody(song, melody);
245
+ };
246
+
247
+ // src/parser.ts
57
248
  var parse = (abc) => {
58
249
  abc = abc.replace(/%.*/g, "").replaceAll(/\n+/g, "\n");
59
250
  const song = new AbcSong();
60
251
  extractInfoFields(abc, song);
61
252
  const tuneObject = convertStringToAbcTune(abc);
62
- if (tuneObject === void 0) {
63
- return void 0;
64
- }
65
253
  song.clef = tuneObject.lines[0].staff[0].clef || song.clef;
66
254
  song.keySignature = tuneObject.lines[0].staff[0].key || song.keySignature;
67
255
  song.melody = tuneObject.lines.map((line) => line.staff[0].voices[0]);
@@ -149,7 +337,7 @@ var extractInfoFields = (abc, song) => {
149
337
  song.transcription = getField(abc, "Z");
150
338
  return abc.replace(/%.*\n/g, "").replace(/(^|\n) *\t*[ABCDFGHIKLMmNOPQRrSsTUVXZ]:.*/g, "").replace(/\n+/g, "\n").replace(/^\n*/g, "").replace(/\n*$/g, "");
151
339
  };
152
- var extractNotesAndLyrics = (abc) => {
340
+ var squashNotesAndLyrics = (abc) => {
153
341
  const notes = [];
154
342
  const lyrics = [];
155
343
  abc.split("\n").map((it) => it.trim()).forEach((it) => {
@@ -164,41 +352,13 @@ var extractNotesAndLyrics = (abc) => {
164
352
  lyrics: lyrics.join(" ")
165
353
  };
166
354
  };
167
- var addInfoFieldsToMelody = (song, abc) => {
168
- let result = "";
169
- result += song.referenceNumber === void 0 ? "" : "X:" + song.referenceNumber + "\n";
170
- result += song.title === void 0 ? "" : "T:" + song.title + "\n";
171
- result += song.area === void 0 ? "" : "A:" + song.area + "\n";
172
- result += song.book === void 0 ? "" : "B:" + song.book + "\n";
173
- result += song.composer === void 0 ? "" : "C:" + song.composer + "\n";
174
- result += song.discography === void 0 ? "" : "D:" + song.discography + "\n";
175
- result += song.fileUrl === void 0 ? "" : "F:" + song.fileUrl + "\n";
176
- result += song.group === void 0 ? "" : "G:" + song.group + "\n";
177
- result += song.history === void 0 ? "" : "H:" + song.history + "\n";
178
- result += song.instruction === void 0 ? "" : "I:" + song.instruction + "\n";
179
- result += song.key === void 0 ? "" : "K:" + song.key + "\n";
180
- result += song.unitNoteLength === void 0 ? "" : "L:" + song.unitNoteLength + "\n";
181
- result += song.meter === void 0 ? "" : "M:" + song.meter + "\n";
182
- result += song.macro === void 0 ? "" : "m:" + song.macro + "\n";
183
- result += song.notes === void 0 ? "" : "N:" + song.notes + "\n";
184
- result += song.origin === void 0 ? "" : "O:" + song.origin + "\n";
185
- result += song.parts === void 0 ? "" : "P:" + song.parts + "\n";
186
- result += song.tempo === void 0 ? "" : "Q:" + song.tempo + "\n";
187
- result += song.rhythm === void 0 ? "" : "R:" + song.rhythm + "\n";
188
- result += song.remark === void 0 ? "" : "r:" + song.remark + "\n";
189
- result += song.source === void 0 ? "" : "S:" + song.source + "\n";
190
- result += song.symbolLine === void 0 ? "" : "s:" + song.symbolLine + "\n";
191
- result += song.userDefined === void 0 ? "" : "U:" + song.userDefined + "\n";
192
- result += song.voice === void 0 ? "" : "V:" + song.voice + "\n";
193
- result += song.transcription === void 0 ? "" : "Z:" + song.transcription + "\n";
194
- return result + abc;
195
- };
196
355
  var convertStringToAbcTune = (abc) => {
197
356
  const objectArray = ABCJS.parseOnly(abc);
198
357
  const object = objectArray;
199
358
  validate(object != null, "Tune object may not be null");
200
359
  validate(object.length > 0, "Tune object may not be empty");
201
360
  validate(object[0].lines != null, "Tune object lines may not be null");
361
+ object[0].lines = object[0].lines.filter((it) => it.staff);
202
362
  validate(object[0].lines.length > 0, "Tune object lines are empty");
203
363
  validate(object[0].lines[0].staff != null, "Staffs may not be null");
204
364
  validate(object[0].lines[0].staff.length > 0, "Staffs are empty");
@@ -246,17 +406,19 @@ var generateAbcForVerse = (verse, activeMelody) => {
246
406
  const melody = getForVerse(activeMelody, verse)?.melody || activeMelody.melody;
247
407
  return combineMelodyAndLyrics(melody, verse.abcLyrics || "");
248
408
  };
249
- export {
409
+ // Annotate the CommonJS export names for ESM import in node:
410
+ 0 && (module.exports = {
250
411
  AbcSong,
251
412
  ValidationError,
252
413
  addInfoFieldsToMelody,
253
414
  combineMelodyAndLyrics,
254
415
  convertStringToAbcTune,
255
416
  extractInfoFields,
256
- extractNotesAndLyrics,
257
417
  generateAbcForVerse,
258
418
  getField,
259
419
  getForVerse,
260
420
  parse,
421
+ squashNotesAndLyrics,
422
+ stringify,
261
423
  validate
262
- };
424
+ });
@@ -3,7 +3,7 @@ import { SynthOptions } from 'abcjs';
3
3
  type AccidentalName = "flat" | "natural" | "sharp" | "dblsharp" | "dblflat" | "quarterflat" | "quartersharp";
4
4
  type ChordPlacement = "above" | "below" | "left" | "right" | "default";
5
5
  type StemDirection = "up" | "down" | "auto" | "none";
6
- type AbcType = "bar_thin" | "bar_thin_thick" | "bar_thin_thin" | "bar_thick_thin" | "bar_right_repeat" | "bar_left_repeat" | "bar_double_repeat";
6
+ type AbcType = "bar_thin" | "bar_thin_thick" | "bar_thin_thin" | "bar_thick_thin" | "bar_right_repeat" | "bar_left_repeat" | "bar_dbl_repeat";
7
7
  type AbcElementType = "note" | "bar";
8
8
  type Clef = "treble" | "tenor" | "bass" | "alto" | "treble+8" | "tenor+8" | "bass+8" | "alto+8" | "treble-8" | "tenor-8" | "bass-8" | "alto-8" | "none" | "perc";
9
9
  type NoteHeadType = "normal" | "harmonic" | "rhythm" | "x" | "triangle";
@@ -44,6 +44,7 @@ interface VoiceItemBar {
44
44
  type: AbcType;
45
45
  startChar: number;
46
46
  endChar: number;
47
+ startEnding?: string;
47
48
  }
48
49
  interface NoteProperties {
49
50
  duration: number;
@@ -51,6 +52,8 @@ interface NoteProperties {
51
52
  lyric?: AbcLyric[];
52
53
  chord?: AbcChord[];
53
54
  rest?: AbcRest;
55
+ startBeam?: boolean;
56
+ endBeam?: boolean;
54
57
  }
55
58
  interface VoiceItemNote extends NoteProperties {
56
59
  el_type: "note";
@@ -220,17 +223,48 @@ declare class AbcSong {
220
223
  clef: AbcClef;
221
224
  keySignature: KeySignature;
222
225
  }
223
- interface NoteGroupInterface {
226
+ type NoteGroupInterface = {
224
227
  notes: string;
225
228
  lyrics: string;
226
- }
229
+ };
227
230
 
228
- declare const parse: (abc: string) => AbcSong | undefined;
231
+ /**
232
+ * Generates an AbcSong object from an ABC notation string.
233
+ * This function does some extra processing to handle slurs and lyrics properly.
234
+ * If you do not want this, use `convertStringToAbcTune` directly.
235
+ *
236
+ * @param abc
237
+ */
238
+ declare const parse: (abc: string) => AbcSong;
239
+ /**
240
+ * Get an info/header field from an ABC notation string.
241
+ * @param abc
242
+ * @param field
243
+ * @param _default
244
+ */
229
245
  declare const getField: (abc: string, field: string, _default?: string) => (string | undefined);
246
+ /**
247
+ * Extract info/header fields from an ABC notation string into an AbcSong object and return the remaining melody string.
248
+ * @param abc
249
+ * @param song
250
+ */
230
251
  declare const extractInfoFields: (abc: string, song: AbcSong) => string;
231
- declare const extractNotesAndLyrics: (abc: string) => NoteGroupInterface;
232
- declare const addInfoFieldsToMelody: (song: AbcSong, abc: string) => string;
252
+ /**
253
+ * Extract notes and lyrics from an ABC notation string as a single line for each.
254
+ * Note that you must first remove info/header fields before using this function.
255
+ * @param abc
256
+ */
257
+ declare const squashNotesAndLyrics: (abc: string) => NoteGroupInterface;
258
+ /**
259
+ * Converts and validates an ABC notation string into a TuneObject.
260
+ * @param abc
261
+ */
233
262
  declare const convertStringToAbcTune: (abc: string) => TuneObject;
263
+ /**
264
+ * Combine multi line lyrics line with a multi line melody into a single ABC notation string.
265
+ * @param melody
266
+ * @param lyrics
267
+ */
234
268
  declare const combineMelodyAndLyrics: (melody: string, lyrics: string) => string;
235
269
 
236
270
  type Verse = {
@@ -253,4 +287,16 @@ declare class ValidationError extends Error {
253
287
  }
254
288
  declare function validate(result: boolean, message?: string): void;
255
289
 
256
- export { type AbcChord, type AbcClef, type AbcElementType, type AbcLine, type AbcLyric, type AbcMelody, type AbcPitch, type AbcPitchStartSlur, type AbcRest, AbcSong, type AbcStaff, type AbcSubMelody, type AbcType, type Accidental, type AccidentalName, type AudioTrack, type AudioTrackNote, type AudioTrackProgram, type AudioTrackText, type AudioTracks, type BracePosition, type ChordPlacement, type ChordType, type Clef, type KeyAccidentalName, type KeyRoot, type KeySignature, type MetaText, type Mode, type NoteGroupInterface, type NoteHeadType, type NoteLetter, type NoteProperties, type StemDirection, type TuneObject, ValidationError, type Verse, type VoiceItem, type VoiceItemBar, type VoiceItemClef, type VoiceItemGap, type VoiceItemKey, type VoiceItemNote, type VoiceItemStem, addInfoFieldsToMelody, combineMelodyAndLyrics, convertStringToAbcTune, extractInfoFields, extractNotesAndLyrics, generateAbcForVerse, getField, getForVerse, parse, validate };
290
+ /**
291
+ * Converts an AbcSong object's info fields to ABC notation and merges them with the given melody string.
292
+ * @param song
293
+ * @param abc
294
+ */
295
+ declare const addInfoFieldsToMelody: (song: AbcSong, abc: string) => string;
296
+ /**
297
+ * Convert an AbcSong object to an ABC notation string.
298
+ * @param song
299
+ */
300
+ declare const stringify: (song: AbcSong) => string;
301
+
302
+ export { type AbcChord, type AbcClef, type AbcElementType, type AbcLine, type AbcLyric, type AbcMelody, type AbcPitch, type AbcPitchStartSlur, type AbcRest, AbcSong, type AbcStaff, type AbcSubMelody, type AbcType, type Accidental, type AccidentalName, type AudioTrack, type AudioTrackNote, type AudioTrackProgram, type AudioTrackText, type AudioTracks, type BracePosition, type ChordPlacement, type ChordType, type Clef, type KeyAccidentalName, type KeyRoot, type KeySignature, type MetaText, type Mode, type NoteGroupInterface, type NoteHeadType, type NoteLetter, type NoteProperties, type StemDirection, type TuneObject, ValidationError, type Verse, type VoiceItem, type VoiceItemBar, type VoiceItemClef, type VoiceItemGap, type VoiceItemKey, type VoiceItemNote, type VoiceItemStem, addInfoFieldsToMelody, combineMelodyAndLyrics, convertStringToAbcTune, extractInfoFields, generateAbcForVerse, getField, getForVerse, parse, squashNotesAndLyrics, stringify, validate };
package/dist/index.d.ts CHANGED
@@ -3,7 +3,7 @@ import { SynthOptions } from 'abcjs';
3
3
  type AccidentalName = "flat" | "natural" | "sharp" | "dblsharp" | "dblflat" | "quarterflat" | "quartersharp";
4
4
  type ChordPlacement = "above" | "below" | "left" | "right" | "default";
5
5
  type StemDirection = "up" | "down" | "auto" | "none";
6
- type AbcType = "bar_thin" | "bar_thin_thick" | "bar_thin_thin" | "bar_thick_thin" | "bar_right_repeat" | "bar_left_repeat" | "bar_double_repeat";
6
+ type AbcType = "bar_thin" | "bar_thin_thick" | "bar_thin_thin" | "bar_thick_thin" | "bar_right_repeat" | "bar_left_repeat" | "bar_dbl_repeat";
7
7
  type AbcElementType = "note" | "bar";
8
8
  type Clef = "treble" | "tenor" | "bass" | "alto" | "treble+8" | "tenor+8" | "bass+8" | "alto+8" | "treble-8" | "tenor-8" | "bass-8" | "alto-8" | "none" | "perc";
9
9
  type NoteHeadType = "normal" | "harmonic" | "rhythm" | "x" | "triangle";
@@ -44,6 +44,7 @@ interface VoiceItemBar {
44
44
  type: AbcType;
45
45
  startChar: number;
46
46
  endChar: number;
47
+ startEnding?: string;
47
48
  }
48
49
  interface NoteProperties {
49
50
  duration: number;
@@ -51,6 +52,8 @@ interface NoteProperties {
51
52
  lyric?: AbcLyric[];
52
53
  chord?: AbcChord[];
53
54
  rest?: AbcRest;
55
+ startBeam?: boolean;
56
+ endBeam?: boolean;
54
57
  }
55
58
  interface VoiceItemNote extends NoteProperties {
56
59
  el_type: "note";
@@ -220,17 +223,48 @@ declare class AbcSong {
220
223
  clef: AbcClef;
221
224
  keySignature: KeySignature;
222
225
  }
223
- interface NoteGroupInterface {
226
+ type NoteGroupInterface = {
224
227
  notes: string;
225
228
  lyrics: string;
226
- }
229
+ };
227
230
 
228
- declare const parse: (abc: string) => AbcSong | undefined;
231
+ /**
232
+ * Generates an AbcSong object from an ABC notation string.
233
+ * This function does some extra processing to handle slurs and lyrics properly.
234
+ * If you do not want this, use `convertStringToAbcTune` directly.
235
+ *
236
+ * @param abc
237
+ */
238
+ declare const parse: (abc: string) => AbcSong;
239
+ /**
240
+ * Get an info/header field from an ABC notation string.
241
+ * @param abc
242
+ * @param field
243
+ * @param _default
244
+ */
229
245
  declare const getField: (abc: string, field: string, _default?: string) => (string | undefined);
246
+ /**
247
+ * Extract info/header fields from an ABC notation string into an AbcSong object and return the remaining melody string.
248
+ * @param abc
249
+ * @param song
250
+ */
230
251
  declare const extractInfoFields: (abc: string, song: AbcSong) => string;
231
- declare const extractNotesAndLyrics: (abc: string) => NoteGroupInterface;
232
- declare const addInfoFieldsToMelody: (song: AbcSong, abc: string) => string;
252
+ /**
253
+ * Extract notes and lyrics from an ABC notation string as a single line for each.
254
+ * Note that you must first remove info/header fields before using this function.
255
+ * @param abc
256
+ */
257
+ declare const squashNotesAndLyrics: (abc: string) => NoteGroupInterface;
258
+ /**
259
+ * Converts and validates an ABC notation string into a TuneObject.
260
+ * @param abc
261
+ */
233
262
  declare const convertStringToAbcTune: (abc: string) => TuneObject;
263
+ /**
264
+ * Combine multi line lyrics line with a multi line melody into a single ABC notation string.
265
+ * @param melody
266
+ * @param lyrics
267
+ */
234
268
  declare const combineMelodyAndLyrics: (melody: string, lyrics: string) => string;
235
269
 
236
270
  type Verse = {
@@ -253,4 +287,16 @@ declare class ValidationError extends Error {
253
287
  }
254
288
  declare function validate(result: boolean, message?: string): void;
255
289
 
256
- export { type AbcChord, type AbcClef, type AbcElementType, type AbcLine, type AbcLyric, type AbcMelody, type AbcPitch, type AbcPitchStartSlur, type AbcRest, AbcSong, type AbcStaff, type AbcSubMelody, type AbcType, type Accidental, type AccidentalName, type AudioTrack, type AudioTrackNote, type AudioTrackProgram, type AudioTrackText, type AudioTracks, type BracePosition, type ChordPlacement, type ChordType, type Clef, type KeyAccidentalName, type KeyRoot, type KeySignature, type MetaText, type Mode, type NoteGroupInterface, type NoteHeadType, type NoteLetter, type NoteProperties, type StemDirection, type TuneObject, ValidationError, type Verse, type VoiceItem, type VoiceItemBar, type VoiceItemClef, type VoiceItemGap, type VoiceItemKey, type VoiceItemNote, type VoiceItemStem, addInfoFieldsToMelody, combineMelodyAndLyrics, convertStringToAbcTune, extractInfoFields, extractNotesAndLyrics, generateAbcForVerse, getField, getForVerse, parse, validate };
290
+ /**
291
+ * Converts an AbcSong object's info fields to ABC notation and merges them with the given melody string.
292
+ * @param song
293
+ * @param abc
294
+ */
295
+ declare const addInfoFieldsToMelody: (song: AbcSong, abc: string) => string;
296
+ /**
297
+ * Convert an AbcSong object to an ABC notation string.
298
+ * @param song
299
+ */
300
+ declare const stringify: (song: AbcSong) => string;
301
+
302
+ export { type AbcChord, type AbcClef, type AbcElementType, type AbcLine, type AbcLyric, type AbcMelody, type AbcPitch, type AbcPitchStartSlur, type AbcRest, AbcSong, type AbcStaff, type AbcSubMelody, type AbcType, type Accidental, type AccidentalName, type AudioTrack, type AudioTrackNote, type AudioTrackProgram, type AudioTrackText, type AudioTracks, type BracePosition, type ChordPlacement, type ChordType, type Clef, type KeyAccidentalName, type KeyRoot, type KeySignature, type MetaText, type Mode, type NoteGroupInterface, type NoteHeadType, type NoteLetter, type NoteProperties, type StemDirection, type TuneObject, ValidationError, type Verse, type VoiceItem, type VoiceItemBar, type VoiceItemClef, type VoiceItemGap, type VoiceItemKey, type VoiceItemNote, type VoiceItemStem, addInfoFieldsToMelody, combineMelodyAndLyrics, convertStringToAbcTune, extractInfoFields, generateAbcForVerse, getField, getForVerse, parse, squashNotesAndLyrics, stringify, validate };
package/dist/index.js CHANGED
@@ -1,51 +1,4 @@
1
- "use strict";
2
- var __create = Object.create;
3
- var __defProp = Object.defineProperty;
4
- var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
- var __getOwnPropNames = Object.getOwnPropertyNames;
6
- var __getProtoOf = Object.getPrototypeOf;
7
- var __hasOwnProp = Object.prototype.hasOwnProperty;
8
- var __export = (target, all) => {
9
- for (var name in all)
10
- __defProp(target, name, { get: all[name], enumerable: true });
11
- };
12
- var __copyProps = (to, from, except, desc) => {
13
- if (from && typeof from === "object" || typeof from === "function") {
14
- for (let key of __getOwnPropNames(from))
15
- if (!__hasOwnProp.call(to, key) && key !== except)
16
- __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
- }
18
- return to;
19
- };
20
- var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
- // If the importer is in node compatibility mode or this is not an ESM
22
- // file that has been converted to a CommonJS file using a Babel-
23
- // compatible transform (i.e. "__esModule" has not been set), then set
24
- // "default" to the CommonJS "module.exports" for node compatibility.
25
- isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
- mod
27
- ));
28
- var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
-
30
- // src/index.ts
31
- var index_exports = {};
32
- __export(index_exports, {
33
- AbcSong: () => AbcSong,
34
- ValidationError: () => ValidationError,
35
- addInfoFieldsToMelody: () => addInfoFieldsToMelody,
36
- combineMelodyAndLyrics: () => combineMelodyAndLyrics,
37
- convertStringToAbcTune: () => convertStringToAbcTune,
38
- extractInfoFields: () => extractInfoFields,
39
- extractNotesAndLyrics: () => extractNotesAndLyrics,
40
- generateAbcForVerse: () => generateAbcForVerse,
41
- getField: () => getField,
42
- getForVerse: () => getForVerse,
43
- parse: () => parse,
44
- validate: () => validate
45
- });
46
- module.exports = __toCommonJS(index_exports);
47
-
48
- // src/abcTypes.ts
1
+ // src/types.ts
49
2
  var AbcSong = class {
50
3
  // See also: https://abcnotation.com/wiki/abc:standard:v2.1#information_fields
51
4
  area;
@@ -87,8 +40,8 @@ var AbcSong = class {
87
40
  };
88
41
  };
89
42
 
90
- // src/abc.ts
91
- var ABCJS = __toESM(require("abcjs"));
43
+ // src/parser.ts
44
+ import * as ABCJS from "abcjs";
92
45
 
93
46
  // src/validation.ts
94
47
  var ValidationError = class extends Error {
@@ -100,15 +53,155 @@ function validate(result, message) {
100
53
  throw new ValidationError(message || "Value is not true");
101
54
  }
102
55
 
103
- // src/abc.ts
56
+ // src/deparser.ts
57
+ var addInfoFieldsToMelody = (song, abc) => {
58
+ let result = "";
59
+ result += song.referenceNumber === void 0 ? "" : "X:" + song.referenceNumber + "\n";
60
+ result += song.title === void 0 ? "" : "T:" + song.title + "\n";
61
+ result += song.area === void 0 ? "" : "A:" + song.area + "\n";
62
+ result += song.book === void 0 ? "" : "B:" + song.book + "\n";
63
+ result += song.composer === void 0 ? "" : "C:" + song.composer + "\n";
64
+ result += song.discography === void 0 ? "" : "D:" + song.discography + "\n";
65
+ result += song.fileUrl === void 0 ? "" : "F:" + song.fileUrl + "\n";
66
+ result += song.group === void 0 ? "" : "G:" + song.group + "\n";
67
+ result += song.history === void 0 ? "" : "H:" + song.history + "\n";
68
+ result += song.instruction === void 0 ? "" : "I:" + song.instruction + "\n";
69
+ result += song.key === void 0 ? "" : "K:" + song.key + "\n";
70
+ result += song.unitNoteLength === void 0 ? "" : "L:" + song.unitNoteLength + "\n";
71
+ result += song.meter === void 0 ? "" : "M:" + song.meter + "\n";
72
+ result += song.macro === void 0 ? "" : "m:" + song.macro + "\n";
73
+ result += song.notes === void 0 ? "" : "N:" + song.notes + "\n";
74
+ result += song.origin === void 0 ? "" : "O:" + song.origin + "\n";
75
+ result += song.parts === void 0 ? "" : "P:" + song.parts + "\n";
76
+ result += song.tempo === void 0 ? "" : "Q:" + song.tempo + "\n";
77
+ result += song.rhythm === void 0 ? "" : "R:" + song.rhythm + "\n";
78
+ result += song.remark === void 0 ? "" : "r:" + song.remark + "\n";
79
+ result += song.source === void 0 ? "" : "S:" + song.source + "\n";
80
+ result += song.symbolLine === void 0 ? "" : "s:" + song.symbolLine + "\n";
81
+ result += song.userDefined === void 0 ? "" : "U:" + song.userDefined + "\n";
82
+ result += song.voice === void 0 ? "" : "V:" + song.voice + "\n";
83
+ result += song.transcription === void 0 ? "" : "Z:" + song.transcription + "\n";
84
+ return result + abc;
85
+ };
86
+ var durationToString = (duration) => {
87
+ duration /= 4;
88
+ if (duration > 3 / 8) return (duration * 4).toString();
89
+ if (duration == 1 / 4) return "";
90
+ if (duration == 3 / 8) return "3/2";
91
+ if (duration == 1 / 8) return "/2";
92
+ if (duration == 3 / 16) return "3/4";
93
+ if (duration == 1 / 16) return "/4";
94
+ if (duration == 3 / 32) return "3/8";
95
+ if (duration == 1 / 32) return "/8";
96
+ if (duration == 3 / 64) return "3/16";
97
+ return "";
98
+ };
99
+ var voiceBarToString = (voice) => {
100
+ let result = "";
101
+ switch (voice.type) {
102
+ case "bar_thin":
103
+ result += "|";
104
+ break;
105
+ case "bar_thin_thick":
106
+ result += "|]";
107
+ break;
108
+ case "bar_thin_thin":
109
+ result += "||";
110
+ break;
111
+ case "bar_thick_thin":
112
+ result += "[|";
113
+ break;
114
+ case "bar_right_repeat":
115
+ result += ":|";
116
+ break;
117
+ case "bar_left_repeat":
118
+ result += "|:";
119
+ break;
120
+ case "bar_dbl_repeat":
121
+ result += "::";
122
+ break;
123
+ }
124
+ if (voice.startEnding) result += voice.startEnding;
125
+ return result + " ";
126
+ };
127
+ var voiceNoteToString = (voice, durationMultiplier, isBeam) => {
128
+ let notes = "";
129
+ let lyrics = "";
130
+ const duration = durationToString(voice.duration / durationMultiplier);
131
+ if (voice.startBeam) isBeam = true;
132
+ if (voice.endBeam) isBeam = false;
133
+ voice.chord?.forEach((chord) => {
134
+ const parsedChordName = chord.name.replaceAll(/♭/g, "b").replaceAll(/♯/g, "#");
135
+ notes += `"${parsedChordName}"`;
136
+ });
137
+ if (voice.pitches && voice.pitches?.length > 1) notes += "[";
138
+ voice.pitches?.forEach((pitch) => {
139
+ let note = pitch.name + duration;
140
+ if (pitch.startSlur) note = "(".repeat(pitch.startSlur.length) + note;
141
+ if (pitch.endSlur) note += ")".repeat(pitch.endSlur.length);
142
+ if (pitch.startTie) note += "-";
143
+ notes += note;
144
+ });
145
+ if (voice.pitches && voice.pitches?.length > 1) notes += "]";
146
+ if (voice.rest) {
147
+ if (voice.rest.type == "spacer") {
148
+ notes += "y";
149
+ } else if (voice.rest.type == "multimeasure") {
150
+ notes += "Z" + durationToString(voice.duration / 4);
151
+ } else {
152
+ notes += "z" + duration;
153
+ }
154
+ }
155
+ if (voice.pitches) {
156
+ voice.lyric?.forEach((lyric) => {
157
+ let text = lyric.syllable.replace(/ /g, "~");
158
+ if (text == "") text = "*";
159
+ lyrics += text + lyric.divider;
160
+ });
161
+ }
162
+ if (!isBeam) notes += " ";
163
+ return {
164
+ notes,
165
+ lyrics,
166
+ isBeam
167
+ };
168
+ };
169
+ var stringify = (song) => {
170
+ const durationParts = (song.unitNoteLength || "1/8").split("/");
171
+ const durationMultiplier = durationParts.length > 1 ? +durationParts[0] / +durationParts[1] : +durationParts[0];
172
+ const melody = song.melody.flatMap((line) => {
173
+ const notes = [];
174
+ const lyrics = [];
175
+ let isBeam = false;
176
+ line.forEach((voice) => {
177
+ switch (voice.el_type) {
178
+ case "bar":
179
+ notes.push(voiceBarToString(voice));
180
+ break;
181
+ case "note":
182
+ const result2 = voiceNoteToString(voice, durationMultiplier, isBeam);
183
+ notes.push(result2.notes);
184
+ lyrics.push(result2.lyrics);
185
+ isBeam = result2.isBeam;
186
+ break;
187
+ }
188
+ });
189
+ const result = [notes.join("").trim()];
190
+ const mergedLyrics = lyrics.join("").trim();
191
+ if (mergedLyrics.length > 0) {
192
+ result.push("w: " + mergedLyrics);
193
+ }
194
+ return result;
195
+ }).join("\n");
196
+ return addInfoFieldsToMelody(song, melody);
197
+ };
198
+
199
+ // src/parser.ts
104
200
  var parse = (abc) => {
105
201
  abc = abc.replace(/%.*/g, "").replaceAll(/\n+/g, "\n");
106
202
  const song = new AbcSong();
107
203
  extractInfoFields(abc, song);
108
204
  const tuneObject = convertStringToAbcTune(abc);
109
- if (tuneObject === void 0) {
110
- return void 0;
111
- }
112
205
  song.clef = tuneObject.lines[0].staff[0].clef || song.clef;
113
206
  song.keySignature = tuneObject.lines[0].staff[0].key || song.keySignature;
114
207
  song.melody = tuneObject.lines.map((line) => line.staff[0].voices[0]);
@@ -196,7 +289,7 @@ var extractInfoFields = (abc, song) => {
196
289
  song.transcription = getField(abc, "Z");
197
290
  return abc.replace(/%.*\n/g, "").replace(/(^|\n) *\t*[ABCDFGHIKLMmNOPQRrSsTUVXZ]:.*/g, "").replace(/\n+/g, "\n").replace(/^\n*/g, "").replace(/\n*$/g, "");
198
291
  };
199
- var extractNotesAndLyrics = (abc) => {
292
+ var squashNotesAndLyrics = (abc) => {
200
293
  const notes = [];
201
294
  const lyrics = [];
202
295
  abc.split("\n").map((it) => it.trim()).forEach((it) => {
@@ -211,41 +304,13 @@ var extractNotesAndLyrics = (abc) => {
211
304
  lyrics: lyrics.join(" ")
212
305
  };
213
306
  };
214
- var addInfoFieldsToMelody = (song, abc) => {
215
- let result = "";
216
- result += song.referenceNumber === void 0 ? "" : "X:" + song.referenceNumber + "\n";
217
- result += song.title === void 0 ? "" : "T:" + song.title + "\n";
218
- result += song.area === void 0 ? "" : "A:" + song.area + "\n";
219
- result += song.book === void 0 ? "" : "B:" + song.book + "\n";
220
- result += song.composer === void 0 ? "" : "C:" + song.composer + "\n";
221
- result += song.discography === void 0 ? "" : "D:" + song.discography + "\n";
222
- result += song.fileUrl === void 0 ? "" : "F:" + song.fileUrl + "\n";
223
- result += song.group === void 0 ? "" : "G:" + song.group + "\n";
224
- result += song.history === void 0 ? "" : "H:" + song.history + "\n";
225
- result += song.instruction === void 0 ? "" : "I:" + song.instruction + "\n";
226
- result += song.key === void 0 ? "" : "K:" + song.key + "\n";
227
- result += song.unitNoteLength === void 0 ? "" : "L:" + song.unitNoteLength + "\n";
228
- result += song.meter === void 0 ? "" : "M:" + song.meter + "\n";
229
- result += song.macro === void 0 ? "" : "m:" + song.macro + "\n";
230
- result += song.notes === void 0 ? "" : "N:" + song.notes + "\n";
231
- result += song.origin === void 0 ? "" : "O:" + song.origin + "\n";
232
- result += song.parts === void 0 ? "" : "P:" + song.parts + "\n";
233
- result += song.tempo === void 0 ? "" : "Q:" + song.tempo + "\n";
234
- result += song.rhythm === void 0 ? "" : "R:" + song.rhythm + "\n";
235
- result += song.remark === void 0 ? "" : "r:" + song.remark + "\n";
236
- result += song.source === void 0 ? "" : "S:" + song.source + "\n";
237
- result += song.symbolLine === void 0 ? "" : "s:" + song.symbolLine + "\n";
238
- result += song.userDefined === void 0 ? "" : "U:" + song.userDefined + "\n";
239
- result += song.voice === void 0 ? "" : "V:" + song.voice + "\n";
240
- result += song.transcription === void 0 ? "" : "Z:" + song.transcription + "\n";
241
- return result + abc;
242
- };
243
307
  var convertStringToAbcTune = (abc) => {
244
308
  const objectArray = ABCJS.parseOnly(abc);
245
309
  const object = objectArray;
246
310
  validate(object != null, "Tune object may not be null");
247
311
  validate(object.length > 0, "Tune object may not be empty");
248
312
  validate(object[0].lines != null, "Tune object lines may not be null");
313
+ object[0].lines = object[0].lines.filter((it) => it.staff);
249
314
  validate(object[0].lines.length > 0, "Tune object lines are empty");
250
315
  validate(object[0].lines[0].staff != null, "Staffs may not be null");
251
316
  validate(object[0].lines[0].staff.length > 0, "Staffs are empty");
@@ -293,18 +358,18 @@ var generateAbcForVerse = (verse, activeMelody) => {
293
358
  const melody = getForVerse(activeMelody, verse)?.melody || activeMelody.melody;
294
359
  return combineMelodyAndLyrics(melody, verse.abcLyrics || "");
295
360
  };
296
- // Annotate the CommonJS export names for ESM import in node:
297
- 0 && (module.exports = {
361
+ export {
298
362
  AbcSong,
299
363
  ValidationError,
300
364
  addInfoFieldsToMelody,
301
365
  combineMelodyAndLyrics,
302
366
  convertStringToAbcTune,
303
367
  extractInfoFields,
304
- extractNotesAndLyrics,
305
368
  generateAbcForVerse,
306
369
  getField,
307
370
  getForVerse,
308
371
  parse,
372
+ squashNotesAndLyrics,
373
+ stringify,
309
374
  validate
310
- });
375
+ };
package/package.json CHANGED
@@ -1,12 +1,18 @@
1
1
  {
2
2
  "name": "@hymnbook/abc",
3
- "version": "0.0.0",
3
+ "version": "0.1.1",
4
4
  "description": "",
5
+ "type": "module",
5
6
  "main": "./dist/index.js",
6
7
  "module": "./dist/index.mjs",
7
8
  "types": "./dist/index.d.ts",
8
9
  "scripts": {
9
- "build": "tsup"
10
+ "test": "node --experimental-vm-modules node_modules/.bin/jest",
11
+ "build": "tsup",
12
+ "publish": "npm run test && npm run build && npm publish --access public",
13
+ "publish-patch": "npm version patch && npm run publish",
14
+ "publish-minor": "npm version minor && npm run publish",
15
+ "publish-major": "npm version major && npm run publish"
10
16
  },
11
17
  "repository": {
12
18
  "type": "git",
@@ -23,7 +29,9 @@
23
29
  },
24
30
  "homepage": "https://github.com/sampie777/hymnbook-abc",
25
31
  "devDependencies": {
26
- "abcjs": "git+https://github.com/paulrosen/abcjs.git#v6.2.3",
32
+ "@jest/globals": "^30.2.0",
33
+ "jest": "^30.2.0",
34
+ "ts-jest": "^29.4.6",
27
35
  "tsup": "^8.5.1",
28
36
  "typescript": "^5.9.3"
29
37
  },
@@ -33,5 +41,14 @@
33
41
  "README.md",
34
42
  "package.json",
35
43
  "package-lock.json"
36
- ]
44
+ ],
45
+ "dependencies": {
46
+ "abcjs": "^6.5.2"
47
+ },
48
+ "exports": {
49
+ ".": {
50
+ "import": "./dist/index.js",
51
+ "require": "./dist/index.cjs"
52
+ }
53
+ }
37
54
  }
package/src/based.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { combineMelodyAndLyrics } from "./abc";
1
+ import { combineMelodyAndLyrics } from "./parser";
2
2
  import { AbcMelody, AbcSubMelody, Verse } from "./basedTypes";
3
3
 
4
4
 
@@ -0,0 +1,186 @@
1
+ import { AbcSong, VoiceItemBar, VoiceItemNote } from "./types";
2
+
3
+ /**
4
+ * Converts an AbcSong object's info fields to ABC notation and merges them with the given melody string.
5
+ * @param song
6
+ * @param abc
7
+ */
8
+ export const addInfoFieldsToMelody = (song: AbcSong, abc: string): string => {
9
+ let result = "";
10
+ // See for following order of fields: https://abcnotation.com/wiki/abc:standard:v2.1#description_of_information_fields
11
+ result += song.referenceNumber === undefined ? "" : "X:" + song.referenceNumber + "\n";
12
+ result += song.title === undefined ? "" : "T:" + song.title + "\n";
13
+ result += song.area === undefined ? "" : "A:" + song.area + "\n";
14
+ result += song.book === undefined ? "" : "B:" + song.book + "\n";
15
+ result += song.composer === undefined ? "" : "C:" + song.composer + "\n";
16
+ result += song.discography === undefined ? "" : "D:" + song.discography + "\n";
17
+ result += song.fileUrl === undefined ? "" : "F:" + song.fileUrl + "\n";
18
+ result += song.group === undefined ? "" : "G:" + song.group + "\n";
19
+ result += song.history === undefined ? "" : "H:" + song.history + "\n";
20
+ result += song.instruction === undefined ? "" : "I:" + song.instruction + "\n";
21
+ result += song.key === undefined ? "" : "K:" + song.key + "\n";
22
+ result += song.unitNoteLength === undefined ? "" : "L:" + song.unitNoteLength + "\n";
23
+ result += song.meter === undefined ? "" : "M:" + song.meter + "\n";
24
+ result += song.macro === undefined ? "" : "m:" + song.macro + "\n";
25
+ result += song.notes === undefined ? "" : "N:" + song.notes + "\n";
26
+ result += song.origin === undefined ? "" : "O:" + song.origin + "\n";
27
+ result += song.parts === undefined ? "" : "P:" + song.parts + "\n";
28
+ result += song.tempo === undefined ? "" : "Q:" + song.tempo + "\n";
29
+ result += song.rhythm === undefined ? "" : "R:" + song.rhythm + "\n";
30
+ result += song.remark === undefined ? "" : "r:" + song.remark + "\n";
31
+ result += song.source === undefined ? "" : "S:" + song.source + "\n";
32
+ result += song.symbolLine === undefined ? "" : "s:" + song.symbolLine + "\n";
33
+ result += song.userDefined === undefined ? "" : "U:" + song.userDefined + "\n";
34
+ result += song.voice === undefined ? "" : "V:" + song.voice + "\n";
35
+ result += song.transcription === undefined ? "" : "Z:" + song.transcription + "\n";
36
+ return result + abc;
37
+ };
38
+
39
+ const durationToString = (duration: number): string => {
40
+ duration /= 4;
41
+ if (duration > 3 / 8) return (duration * 4).toString();
42
+ if (duration == 1 / 4) return "";
43
+ if (duration == 3 / 8) return "3/2";
44
+ if (duration == 1 / 8) return "/2";
45
+ if (duration == 3 / 16) return "3/4";
46
+ if (duration == 1 / 16) return "/4";
47
+ if (duration == 3 / 32) return "3/8";
48
+ if (duration == 1 / 32) return "/8";
49
+ if (duration == 3 / 64) return "3/16";
50
+
51
+ return ""
52
+ }
53
+
54
+ const voiceBarToString = (voice: VoiceItemBar): string => {
55
+ let result = "";
56
+
57
+ switch (voice.type) {
58
+ case "bar_thin":
59
+ result += "|"
60
+ break;
61
+ case "bar_thin_thick":
62
+ result += "|]"
63
+ break;
64
+ case "bar_thin_thin":
65
+ result += "||"
66
+ break;
67
+ case "bar_thick_thin":
68
+ result += "[|"
69
+ break;
70
+ case "bar_right_repeat":
71
+ result += ":|"
72
+ break;
73
+ case "bar_left_repeat":
74
+ result += "|:"
75
+ break;
76
+ case "bar_dbl_repeat":
77
+ result += "::"
78
+ break;
79
+ }
80
+
81
+ if (voice.startEnding) result += voice.startEnding
82
+
83
+ return result + " ";
84
+ };
85
+
86
+ const voiceNoteToString = (voice: VoiceItemNote, durationMultiplier: number, isBeam: boolean) => {
87
+ let notes = "";
88
+ let lyrics = "";
89
+
90
+ const duration = durationToString(voice.duration / durationMultiplier);
91
+
92
+ if (voice.startBeam) isBeam = true;
93
+ if (voice.endBeam) isBeam = false;
94
+
95
+ /* Process chords */
96
+ voice.chord?.forEach(chord => {
97
+ const parsedChordName = chord.name
98
+ .replaceAll(/♭/g, "b")
99
+ .replaceAll(/♯/g, "#")
100
+ notes += `"${parsedChordName}"`;
101
+ })
102
+
103
+ /* Process notes */
104
+ if (voice.pitches && voice.pitches?.length > 1) notes += "["
105
+ voice.pitches?.forEach(pitch => {
106
+ let note = pitch.name + duration;
107
+
108
+ if (pitch.startSlur) note = "(".repeat(pitch.startSlur.length) + note;
109
+ if (pitch.endSlur) note += ")".repeat(pitch.endSlur.length)
110
+
111
+ if (pitch.startTie) note += "-";
112
+
113
+ notes += note;
114
+ })
115
+ if (voice.pitches && voice.pitches?.length > 1) notes += "]"
116
+
117
+ /* Process rests */
118
+ if (voice.rest) {
119
+ if (voice.rest.type == "spacer") {
120
+ notes += "y";
121
+ } else if (voice.rest.type == "multimeasure") {
122
+ notes += "Z" + durationToString(voice.duration / 4)
123
+ } else {
124
+ notes += "z" + duration;
125
+ }
126
+ }
127
+
128
+ /* Process lyrics */
129
+ if (voice.pitches) {
130
+ voice.lyric?.forEach(lyric => {
131
+ let text = lyric.syllable.replace(/ /g, "~");
132
+ if (text == "") text = "*";
133
+ lyrics += text + lyric.divider
134
+ })
135
+ }
136
+
137
+ if (!isBeam) notes += " ";
138
+
139
+ return {
140
+ notes: notes,
141
+ lyrics: lyrics,
142
+ isBeam: isBeam,
143
+ }
144
+ };
145
+
146
+ /**
147
+ * Convert an AbcSong object to an ABC notation string.
148
+ * @param song
149
+ */
150
+ export const stringify = (song: AbcSong) => {
151
+ const durationParts = (song.unitNoteLength || "1/8").split("/");
152
+ const durationMultiplier = durationParts.length > 1
153
+ ? +durationParts[0] / +durationParts[1]
154
+ : +durationParts[0];
155
+
156
+ const melody = song.melody.flatMap(line => {
157
+ const notes: string[] = [];
158
+ const lyrics: string[] = [];
159
+ let isBeam = false;
160
+
161
+ line.forEach(voice => {
162
+ switch (voice.el_type) {
163
+ case "bar":
164
+ notes.push(voiceBarToString(voice))
165
+ break;
166
+ case "note":
167
+ const result = voiceNoteToString(voice, durationMultiplier, isBeam)
168
+ notes.push(result.notes)
169
+ lyrics.push(result.lyrics)
170
+ isBeam = result.isBeam
171
+ break;
172
+ }
173
+ })
174
+
175
+ const result: string[] = [notes.join("").trim()]
176
+ const mergedLyrics = lyrics.join("").trim()
177
+ if (mergedLyrics.length > 0) {
178
+ result.push("w: " + mergedLyrics)
179
+ }
180
+
181
+ return result;
182
+ })
183
+ .join("\n")
184
+
185
+ return addInfoFieldsToMelody(song, melody);
186
+ };
package/src/index.ts CHANGED
@@ -1,5 +1,6 @@
1
- export * from './abcTypes';
2
- export * from './abc';
1
+ export * from './types';
2
+ export * from './parser';
3
3
  export * from './basedTypes';
4
4
  export * from './based';
5
- export * from './validation';
5
+ export * from './validation';
6
+ export * from './deparser';
@@ -8,13 +8,20 @@ import {
8
8
  TuneObject,
9
9
  VoiceItem,
10
10
  VoiceItemNote
11
- } from "./abcTypes";
11
+ } from "./types";
12
12
  import { validate } from "./validation";
13
+ import { addInfoFieldsToMelody } from "./deparser";
13
14
 
14
15
  // See also https://abcnotation.com/examples
15
16
 
16
-
17
- export const parse = (abc: string): AbcSong | undefined => {
17
+ /**
18
+ * Generates an AbcSong object from an ABC notation string.
19
+ * This function does some extra processing to handle slurs and lyrics properly.
20
+ * If you do not want this, use `convertStringToAbcTune` directly.
21
+ *
22
+ * @param abc
23
+ */
24
+ export const parse = (abc: string): AbcSong => {
18
25
  // Remove comments
19
26
  abc = abc
20
27
  .replace(/%.*/g, "")
@@ -24,9 +31,6 @@ export const parse = (abc: string): AbcSong | undefined => {
24
31
  extractInfoFields(abc, song);
25
32
 
26
33
  const tuneObject = convertStringToAbcTune(abc);
27
- if (tuneObject === undefined) {
28
- return undefined;
29
- }
30
34
 
31
35
  // Get the first staff only (thus in case of multiple instrument play, only take the first instrument)
32
36
  song.clef = tuneObject.lines!![0].staff!![0].clef || song.clef;
@@ -118,6 +122,12 @@ const processSlursForLine = (line: VoiceItem[]) => {
118
122
  });
119
123
  };
120
124
 
125
+ /**
126
+ * Get an info/header field from an ABC notation string.
127
+ * @param abc
128
+ * @param field
129
+ * @param _default
130
+ */
121
131
  export const getField = (abc: string, field: string, _default?: string): (string | undefined) => {
122
132
  const result = abc.match(new RegExp("(^|\n) *\t*" + field + ":(.*)?"));
123
133
  if (result == null || result.length !== 3 || result[2] == null) {
@@ -126,6 +136,11 @@ export const getField = (abc: string, field: string, _default?: string): (string
126
136
  return result[2].trim();
127
137
  };
128
138
 
139
+ /**
140
+ * Extract info/header fields from an ABC notation string into an AbcSong object and return the remaining melody string.
141
+ * @param abc
142
+ * @param song
143
+ */
129
144
  export const extractInfoFields = (abc: string, song: AbcSong): string => {
130
145
  song.area = getField(abc, "A");
131
146
  song.book = getField(abc, "B");
@@ -152,6 +167,7 @@ export const extractInfoFields = (abc: string, song: AbcSong): string => {
152
167
  song.voice = getField(abc, "V");
153
168
  song.referenceNumber = getField(abc, "X", "1");
154
169
  song.transcription = getField(abc, "Z");
170
+
155
171
  return abc
156
172
  .replace(/%.*\n/g, "")
157
173
  .replace(/(^|\n) *\t*[ABCDFGHIKLMmNOPQRrSsTUVXZ]:.*/g, "")
@@ -160,7 +176,12 @@ export const extractInfoFields = (abc: string, song: AbcSong): string => {
160
176
  .replace(/\n*$/g, "");
161
177
  };
162
178
 
163
- export const extractNotesAndLyrics = (abc: string): NoteGroupInterface => {
179
+ /**
180
+ * Extract notes and lyrics from an ABC notation string as a single line for each.
181
+ * Note that you must first remove info/header fields before using this function.
182
+ * @param abc
183
+ */
184
+ export const squashNotesAndLyrics = (abc: string): NoteGroupInterface => {
164
185
  const notes: string[] = [];
165
186
  const lyrics: string[] = [];
166
187
  abc.split("\n")
@@ -176,40 +197,13 @@ export const extractNotesAndLyrics = (abc: string): NoteGroupInterface => {
176
197
  return {
177
198
  notes: notes.join(" "),
178
199
  lyrics: lyrics.join(" ")
179
- } as NoteGroupInterface;
180
- };
181
-
182
- export const addInfoFieldsToMelody = (song: AbcSong, abc: string): string => {
183
- let result = "";
184
- // See for following order of fields: https://abcnotation.com/wiki/abc:standard:v2.1#description_of_information_fields
185
- result += song.referenceNumber === undefined ? "" : "X:" + song.referenceNumber + "\n";
186
- result += song.title === undefined ? "" : "T:" + song.title + "\n";
187
- result += song.area === undefined ? "" : "A:" + song.area + "\n";
188
- result += song.book === undefined ? "" : "B:" + song.book + "\n";
189
- result += song.composer === undefined ? "" : "C:" + song.composer + "\n";
190
- result += song.discography === undefined ? "" : "D:" + song.discography + "\n";
191
- result += song.fileUrl === undefined ? "" : "F:" + song.fileUrl + "\n";
192
- result += song.group === undefined ? "" : "G:" + song.group + "\n";
193
- result += song.history === undefined ? "" : "H:" + song.history + "\n";
194
- result += song.instruction === undefined ? "" : "I:" + song.instruction + "\n";
195
- result += song.key === undefined ? "" : "K:" + song.key + "\n";
196
- result += song.unitNoteLength === undefined ? "" : "L:" + song.unitNoteLength + "\n";
197
- result += song.meter === undefined ? "" : "M:" + song.meter + "\n";
198
- result += song.macro === undefined ? "" : "m:" + song.macro + "\n";
199
- result += song.notes === undefined ? "" : "N:" + song.notes + "\n";
200
- result += song.origin === undefined ? "" : "O:" + song.origin + "\n";
201
- result += song.parts === undefined ? "" : "P:" + song.parts + "\n";
202
- result += song.tempo === undefined ? "" : "Q:" + song.tempo + "\n";
203
- result += song.rhythm === undefined ? "" : "R:" + song.rhythm + "\n";
204
- result += song.remark === undefined ? "" : "r:" + song.remark + "\n";
205
- result += song.source === undefined ? "" : "S:" + song.source + "\n";
206
- result += song.symbolLine === undefined ? "" : "s:" + song.symbolLine + "\n";
207
- result += song.userDefined === undefined ? "" : "U:" + song.userDefined + "\n";
208
- result += song.voice === undefined ? "" : "V:" + song.voice + "\n";
209
- result += song.transcription === undefined ? "" : "Z:" + song.transcription + "\n";
210
- return result + abc;
200
+ };
211
201
  };
212
202
 
203
+ /**
204
+ * Converts and validates an ABC notation string into a TuneObject.
205
+ * @param abc
206
+ */
213
207
  export const convertStringToAbcTune = (abc: string): TuneObject => {
214
208
  const objectArray: TuneObjectArray = ABCJS.parseOnly(abc);
215
209
  // Convert types
@@ -218,6 +212,9 @@ export const convertStringToAbcTune = (abc: string): TuneObject => {
218
212
  validate(object != null, "Tune object may not be null");
219
213
  validate(object.length > 0, "Tune object may not be empty");
220
214
  validate(object[0].lines != null, "Tune object lines may not be null");
215
+
216
+ object[0].lines = object[0].lines.filter(it => it.staff); // Remove empty lines without staff
217
+
221
218
  validate(object[0].lines.length > 0, "Tune object lines are empty");
222
219
  validate(object[0].lines[0].staff != null, "Staffs may not be null");
223
220
  validate(object[0].lines[0].staff!!.length > 0, "Staffs are empty");
@@ -246,7 +243,11 @@ const processAbcLyrics = (object: Array<TuneObject>) => {
246
243
  );
247
244
  };
248
245
 
249
- // Match each lyric line with each melody line
246
+ /**
247
+ * Combine multi line lyrics line with a multi line melody into a single ABC notation string.
248
+ * @param melody
249
+ * @param lyrics
250
+ */
250
251
  export const combineMelodyAndLyrics = (melody: string, lyrics: string): string => {
251
252
  const song = new AbcSong();
252
253
  const rawMelody = extractInfoFields(melody, song);
@@ -12,7 +12,7 @@ export type AbcType =
12
12
  | "bar_thick_thin"
13
13
  | "bar_right_repeat"
14
14
  | "bar_left_repeat"
15
- | "bar_double_repeat";
15
+ | "bar_dbl_repeat";
16
16
  export type AbcElementType = "note" | "bar";
17
17
  export type Clef =
18
18
  "treble"
@@ -99,6 +99,7 @@ export interface VoiceItemBar {
99
99
  type: AbcType;
100
100
  startChar: number;
101
101
  endChar: number;
102
+ startEnding?: string; // Labels the repeat section
102
103
  }
103
104
 
104
105
  export interface NoteProperties {
@@ -107,6 +108,8 @@ export interface NoteProperties {
107
108
  lyric?: AbcLyric[];
108
109
  chord?: AbcChord[];
109
110
  rest?: AbcRest;
111
+ startBeam?: boolean;
112
+ endBeam?: boolean;
110
113
  }
111
114
 
112
115
  export interface VoiceItemNote extends NoteProperties {
@@ -308,7 +311,7 @@ export class AbcSong {
308
311
  };
309
312
  }
310
313
 
311
- export interface NoteGroupInterface {
312
- notes: string,
314
+ export type NoteGroupInterface = {
315
+ notes: string
313
316
  lyrics: string
314
317
  }