diff --git a/package-lock.json b/package-lock.json index c8316c1..6598b4e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "@mui/material": "^5.4.2", "@reduxjs/toolkit": "^1.7.2", "file-saver": "^2.0.5", + "js2xmlparser": "^4.0.2", "just-debounce-it": "^3.0.1", "next": "^12.1.0", "papaparse": "^5.3.1", @@ -3096,6 +3097,14 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/js2xmlparser": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/js2xmlparser/-/js2xmlparser-4.0.2.tgz", + "integrity": "sha512-6n4D8gLlLf1n5mNLQPRfViYzu9RATblzPEtm1SthMX1Pjao0r9YI9nw7ZIfRxQMERS87mcswrg+r/OYrPRX6jA==", + "dependencies": { + "xmlcreate": "^2.0.4" + } + }, "node_modules/jsesc": { "version": "2.5.2", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", @@ -4344,6 +4353,11 @@ "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", "dev": true }, + "node_modules/xmlcreate": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/xmlcreate/-/xmlcreate-2.0.4.tgz", + "integrity": "sha512-nquOebG4sngPmGPICTS5EnxqhKbCmz5Ox5hsszI2T6U5qdrJizBc+0ilYSEjTSzU0yZcmvppztXe/5Al5fUwdg==" + }, "node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", @@ -6514,6 +6528,14 @@ "argparse": "^2.0.1" } }, + "js2xmlparser": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/js2xmlparser/-/js2xmlparser-4.0.2.tgz", + "integrity": "sha512-6n4D8gLlLf1n5mNLQPRfViYzu9RATblzPEtm1SthMX1Pjao0r9YI9nw7ZIfRxQMERS87mcswrg+r/OYrPRX6jA==", + "requires": { + "xmlcreate": "^2.0.4" + } + }, "jsesc": { "version": "2.5.2", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", @@ -7397,6 +7419,11 @@ "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", "dev": true }, + "xmlcreate": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/xmlcreate/-/xmlcreate-2.0.4.tgz", + "integrity": "sha512-nquOebG4sngPmGPICTS5EnxqhKbCmz5Ox5hsszI2T6U5qdrJizBc+0ilYSEjTSzU0yZcmvppztXe/5Al5fUwdg==" + }, "yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", diff --git a/package.json b/package.json index 2155cb8..d3b801d 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "@mui/material": "^5.4.2", "@reduxjs/toolkit": "^1.7.2", "file-saver": "^2.0.5", + "js2xmlparser": "^4.0.2", "just-debounce-it": "^3.0.1", "next": "^12.1.0", "papaparse": "^5.3.1", diff --git a/src/lib/fileExport/exporters/mei.ts b/src/lib/fileExport/exporters/mei.ts index c078bae..e3b5966 100644 --- a/src/lib/fileExport/exporters/mei.ts +++ b/src/lib/fileExport/exporters/mei.ts @@ -1,10 +1,78 @@ +import { getMarkersWithMeasures } from "lib/getMarkersWithMeasures"; import type { FileExporter } from ".."; +const replaceSpacesWithUnderscores = (string: string) => + string.replaceAll(" ", "_"); export const meiExporter: FileExporter = { - fileType: "mei", + fileType: "xml", mimeType: "text/xml", name: "MEI", - stateToFile: async () => { - return "MEI"; + stateToFile: async (state) => { + const js2xmlparser = await import("js2xmlparser"); + + const { markers, duration } = state.player; + const extendedMarkers = getMarkersWithMeasures(markers); + + const { name } = state.app.file; + const nameWithoutFileExtension = name.split(".").slice(0, -1).join("."); + + const resultObject = { + "@": { + xmlns: "http://www.music-encoding.org/ns/mei", + "xml:id": replaceSpacesWithUnderscores(nameWithoutFileExtension), + }, + music: { + performance: { + recording: { + "@": { + "xml:id": "some_recording_id", + decls: "#some_other_recording_id", + }, + avFile: { + "@": { + // XXX: This includes the file extension + target: name, + }, + }, + clip: extendedMarkers.map((marker, i, arr) => ({ + "@": { + begin: marker.time, + end: arr[i + 1]?.time ?? duration, + betype: "time", + startid: replaceSpacesWithUnderscores( + `#${nameWithoutFileExtension}_measure${marker.measure}` + ), + type: "measure", + }, + })), + }, + }, + body: { + mdiv: { + "@": { + "xml:id": replaceSpacesWithUnderscores(name), + label: name, + }, + score: { + section: { + measure: extendedMarkers.map((marker) => ({ + "@": { + "xml:id": replaceSpacesWithUnderscores( + `${nameWithoutFileExtension}_measure${marker.measure}` + ), + sameas: `../some_file_path.xml#some_id_measure${marker.measure}`, + }, + })), + }, + }, + }, + }, + }, + }; + + return js2xmlparser.parse("mei", resultObject, { + declaration: { encoding: "UTF-8" }, + format: { doubleQuotes: true }, + }); }, }; diff --git a/src/lib/fileExport/index.ts b/src/lib/fileExport/index.ts index d9ef5de..39ee9eb 100644 --- a/src/lib/fileExport/index.ts +++ b/src/lib/fileExport/index.ts @@ -16,7 +16,7 @@ export const fileExporters: readonly FileExporter[] = [ textExporter, csvExporter, jsonExporter, - // meiExporter, + meiExporter, ] as const; export const exportOptions = fileExporters.map((exporter) => exporter.fileType); diff --git a/src/state/playerSlice.ts b/src/state/playerSlice.ts index b2d15e7..4a6c05e 100644 --- a/src/state/playerSlice.ts +++ b/src/state/playerSlice.ts @@ -91,6 +91,9 @@ export const playerSlice = createSlice({ builder.addCase(exportAsFile.fulfilled, () => { return; }); + builder.addCase(exportAsFile.rejected, (_, payload) => { + console.error(payload.error); + }); }, });