@hymnbook/abc 0.0.1 → 0.2.0

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.
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");
@@ -268,10 +333,10 @@ var processAbcLyrics = (object) => {
268
333
  )
269
334
  );
270
335
  };
271
- var combineMelodyAndLyrics = (melody, lyrics) => {
336
+ var combineMelodyAndLyrics = (melody, lyrics, options = { trimLines: false }) => {
272
337
  const song = new AbcSong();
273
338
  const rawMelody = extractInfoFields(melody, song);
274
- const melodyLines = rawMelody.replaceAll(/\n+/g, "\n").trim().split("\n");
339
+ const melodyLines = rawMelody.replaceAll(/\n+/g, "\n").trim().split("\n").map((it) => it.trim()).map((it) => options.trimLines ? it.replaceAll(/(^y+|y+$)*/gi, "").replaceAll(/ *y* *(\|+]*) *y* */gi, " $1 ").trim() : it);
275
340
  const lyricLines = lyrics.replaceAll(/\n+/g, "\n").trim().split("\n");
276
341
  const mixedMelody = [];
277
342
  for (let i = 0; i < Math.max(melodyLines.length, lyricLines.length); i++) {
@@ -286,25 +351,25 @@ var getForVerse = (melody, verse) => melody.subMelodies.find((it) => (
286
351
  // `.includes()` won't work due to the Realm data type of `verseUuids`.
287
352
  it.verseUuids.some((it2) => it2 == verse.uuid)
288
353
  ));
289
- var generateAbcForVerse = (verse, activeMelody) => {
354
+ var generateAbcForVerse = (verse, activeMelody, options = { trimLines: false }) => {
290
355
  if (activeMelody === void 0) {
291
356
  return "";
292
357
  }
293
358
  const melody = getForVerse(activeMelody, verse)?.melody || activeMelody.melody;
294
- return combineMelodyAndLyrics(melody, verse.abcLyrics || "");
359
+ return combineMelodyAndLyrics(melody, verse.abcLyrics || "", options);
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,19 @@
1
1
  {
2
2
  "name": "@hymnbook/abc",
3
- "version": "0.0.1",
3
+ "version": "0.2.0",
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
+ "link": "npm run build && rm -f /Users/samuel/workspace/js/hymnbook2/node_modules/@hymnbook/abc/dist/*; ln /Users/samuel/workspace/js/hymnbook-abc/dist/* /Users/samuel/workspace/js/hymnbook2/node_modules/@hymnbook/abc/dist/",
13
+ "build-publish": "npm run build && npm publish --access public",
14
+ "publish-patch": "npm run test && npm version patch && npm run build-publish",
15
+ "publish-minor": "npm run test && npm version minor && npm run build-publish",
16
+ "publish-major": "npm run test && npm version major && npm run build-publish"
10
17
  },
11
18
  "repository": {
12
19
  "type": "git",
@@ -23,6 +30,9 @@
23
30
  },
24
31
  "homepage": "https://github.com/sampie777/hymnbook-abc",
25
32
  "devDependencies": {
33
+ "@jest/globals": "^30.2.0",
34
+ "jest": "^30.2.0",
35
+ "ts-jest": "^29.4.6",
26
36
  "tsup": "^8.5.1",
27
37
  "typescript": "^5.9.3"
28
38
  },
@@ -35,5 +45,11 @@
35
45
  ],
36
46
  "dependencies": {
37
47
  "abcjs": "^6.5.2"
48
+ },
49
+ "exports": {
50
+ ".": {
51
+ "import": "./dist/index.js",
52
+ "require": "./dist/index.cjs"
53
+ }
38
54
  }
39
55
  }
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
 
@@ -7,13 +7,21 @@ export const getForVerse = (melody: AbcMelody, verse: Verse): AbcSubMelody | und
7
7
  // `.includes()` won't work due to the Realm data type of `verseUuids`.
8
8
  it.verseUuids.some(it => it == verse.uuid));
9
9
 
10
+ /**
11
+ * Combine an ABC melody with the lyrics of a verse into a single ABC notation string.
12
+ *
13
+ * @param verse
14
+ * @param activeMelody - The active melody to use. The method returns an empty string if this is undefined.
15
+ * @param options { trimLines?: boolean } - Whether to trim `y` spacers from start/end of lines.
16
+ */
10
17
  export const generateAbcForVerse = (
11
18
  verse: Verse,
12
- activeMelody?: AbcMelody
19
+ activeMelody?: AbcMelody,
20
+ options: { trimLines?: boolean } = { trimLines: false },
13
21
  ): string => {
14
22
  if (activeMelody === undefined) {
15
23
  return "";
16
24
  }
17
25
  const melody = getForVerse(activeMelody, verse)?.melody || activeMelody.melody;
18
- return combineMelodyAndLyrics(melody, verse.abcLyrics || "")
26
+ return combineMelodyAndLyrics(melody, verse.abcLyrics || "", options)
19
27
  };
@@ -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';