Initial commit - Church Music Database
This commit is contained in:
35
new-site/frontend/src/utils/api.js
Normal file
35
new-site/frontend/src/utils/api.js
Normal file
@@ -0,0 +1,35 @@
|
||||
import axios from "axios";
|
||||
|
||||
const api = axios.create({
|
||||
baseURL: "/api",
|
||||
timeout: 30000, // 30 seconds for slow operations like saving large lyrics
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
// Request interceptor to add auth token
|
||||
api.interceptors.request.use(
|
||||
(config) => {
|
||||
const token = localStorage.getItem("authToken");
|
||||
if (token) {
|
||||
config.headers.Authorization = `Bearer ${token}`;
|
||||
}
|
||||
return config;
|
||||
},
|
||||
(error) => Promise.reject(error),
|
||||
);
|
||||
|
||||
// Response interceptor for error handling
|
||||
api.interceptors.response.use(
|
||||
(response) => response,
|
||||
(error) => {
|
||||
if (error.response?.status === 401) {
|
||||
localStorage.removeItem("authToken");
|
||||
window.location.href = "/login";
|
||||
}
|
||||
return Promise.reject(error);
|
||||
},
|
||||
);
|
||||
|
||||
export default api;
|
||||
214
new-site/frontend/src/utils/biometric.js
Normal file
214
new-site/frontend/src/utils/biometric.js
Normal file
@@ -0,0 +1,214 @@
|
||||
/**
|
||||
* Biometric Authentication Utility
|
||||
* Supports Face ID, Touch ID, Windows Hello, and Android Biometric
|
||||
*/
|
||||
|
||||
// Check if biometric authentication is available
|
||||
export const isBiometricAvailable = async () => {
|
||||
// Check if browser supports Web Authentication API
|
||||
if (!window.PublicKeyCredential) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
// Check if platform authenticator is available (Face ID, Touch ID, etc.)
|
||||
const available =
|
||||
await PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable();
|
||||
return available;
|
||||
} catch (error) {
|
||||
// Silently fail for production
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
// Get biometric type description
|
||||
export const getBiometricType = () => {
|
||||
const ua = navigator.userAgent;
|
||||
|
||||
if (/iPhone|iPad|iPod/.test(ua)) {
|
||||
// iOS devices - could be Face ID or Touch ID
|
||||
return "Biometric Authentication";
|
||||
} else if (/Android/.test(ua)) {
|
||||
return "Biometric Authentication";
|
||||
} else if (/Windows/.test(ua)) {
|
||||
return "Biometric Authentication";
|
||||
} else if (/Mac/.test(ua)) {
|
||||
return "Biometric Authentication";
|
||||
}
|
||||
|
||||
return "Biometric Authentication";
|
||||
};
|
||||
|
||||
// Register biometric credential
|
||||
export const registerBiometric = async (username, userId) => {
|
||||
try {
|
||||
// Generate challenge from server (in production, get this from server)
|
||||
const challenge = new Uint8Array(32);
|
||||
crypto.getRandomValues(challenge);
|
||||
|
||||
const publicKeyCredentialCreationOptions = {
|
||||
challenge,
|
||||
rp: {
|
||||
name: "HOP Worship Platform",
|
||||
id: window.location.hostname,
|
||||
},
|
||||
user: {
|
||||
id: new TextEncoder().encode(userId.toString()),
|
||||
name: username,
|
||||
displayName: username,
|
||||
},
|
||||
pubKeyCredParams: [
|
||||
{ alg: -7, type: "public-key" }, // ES256
|
||||
{ alg: -257, type: "public-key" }, // RS256
|
||||
],
|
||||
authenticatorSelection: {
|
||||
authenticatorAttachment: "platform", // Use platform authenticator (Face ID, Touch ID, etc.)
|
||||
userVerification: "required",
|
||||
requireResidentKey: false,
|
||||
},
|
||||
timeout: 60000,
|
||||
attestation: "none",
|
||||
};
|
||||
|
||||
const credential = await navigator.credentials.create({
|
||||
publicKey: publicKeyCredentialCreationOptions,
|
||||
});
|
||||
|
||||
// Convert credential to base64 for storage
|
||||
const credentialData = {
|
||||
id: credential.id,
|
||||
rawId: arrayBufferToBase64(credential.rawId),
|
||||
type: credential.type,
|
||||
response: {
|
||||
attestationObject: arrayBufferToBase64(
|
||||
credential.response.attestationObject,
|
||||
),
|
||||
clientDataJSON: arrayBufferToBase64(credential.response.clientDataJSON),
|
||||
},
|
||||
};
|
||||
|
||||
return credentialData;
|
||||
} catch (error) {
|
||||
console.error("Biometric registration error:", error);
|
||||
throw new Error(getBiometricErrorMessage(error));
|
||||
}
|
||||
};
|
||||
|
||||
// Authenticate using biometric
|
||||
export const authenticateWithBiometric = async (credentialId) => {
|
||||
try {
|
||||
// Generate challenge
|
||||
const challenge = new Uint8Array(32);
|
||||
crypto.getRandomValues(challenge);
|
||||
|
||||
const publicKeyCredentialRequestOptions = {
|
||||
challenge,
|
||||
allowCredentials: credentialId
|
||||
? [
|
||||
{
|
||||
id: base64ToArrayBuffer(credentialId),
|
||||
type: "public-key",
|
||||
transports: ["internal"],
|
||||
},
|
||||
]
|
||||
: [],
|
||||
userVerification: "required",
|
||||
timeout: 60000,
|
||||
};
|
||||
|
||||
const assertion = await navigator.credentials.get({
|
||||
publicKey: publicKeyCredentialRequestOptions,
|
||||
});
|
||||
|
||||
// Convert assertion to base64
|
||||
const assertionData = {
|
||||
id: assertion.id,
|
||||
rawId: arrayBufferToBase64(assertion.rawId),
|
||||
type: assertion.type,
|
||||
response: {
|
||||
authenticatorData: arrayBufferToBase64(
|
||||
assertion.response.authenticatorData,
|
||||
),
|
||||
clientDataJSON: arrayBufferToBase64(assertion.response.clientDataJSON),
|
||||
signature: arrayBufferToBase64(assertion.response.signature),
|
||||
userHandle: assertion.response.userHandle
|
||||
? arrayBufferToBase64(assertion.response.userHandle)
|
||||
: null,
|
||||
},
|
||||
};
|
||||
|
||||
return assertionData;
|
||||
} catch (error) {
|
||||
console.error("Biometric authentication error:", error);
|
||||
throw new Error(getBiometricErrorMessage(error));
|
||||
}
|
||||
};
|
||||
|
||||
// Helper: Convert ArrayBuffer to Base64
|
||||
function arrayBufferToBase64(buffer) {
|
||||
const bytes = new Uint8Array(buffer);
|
||||
let binary = "";
|
||||
for (let i = 0; i < bytes.byteLength; i++) {
|
||||
binary += String.fromCharCode(bytes[i]);
|
||||
}
|
||||
return btoa(binary);
|
||||
}
|
||||
|
||||
// Helper: Convert Base64 to ArrayBuffer
|
||||
function base64ToArrayBuffer(base64) {
|
||||
const binary = atob(base64);
|
||||
const bytes = new Uint8Array(binary.length);
|
||||
for (let i = 0; i < binary.length; i++) {
|
||||
bytes[i] = binary.charCodeAt(i);
|
||||
}
|
||||
return bytes.buffer;
|
||||
}
|
||||
|
||||
// Get user-friendly error message
|
||||
function getBiometricErrorMessage(error) {
|
||||
if (error.name === "NotAllowedError") {
|
||||
return "Biometric authentication was cancelled";
|
||||
} else if (error.name === "NotSupportedError") {
|
||||
return "Biometric authentication is not supported on this device";
|
||||
} else if (error.name === "SecurityError") {
|
||||
return "Biometric authentication failed due to security restrictions";
|
||||
} else if (error.name === "AbortError") {
|
||||
return "Biometric authentication was aborted";
|
||||
} else if (error.name === "InvalidStateError") {
|
||||
return "Biometric credential already registered";
|
||||
} else if (error.name === "NotReadableError") {
|
||||
return "Biometric sensor is not readable";
|
||||
}
|
||||
return error.message || "Biometric authentication failed";
|
||||
}
|
||||
|
||||
// Store biometric credential ID locally
|
||||
export const storeBiometricCredential = (username, credentialId) => {
|
||||
const credentials = getBiometricCredentials();
|
||||
credentials[username] = credentialId;
|
||||
localStorage.setItem("biometric_credentials", JSON.stringify(credentials));
|
||||
};
|
||||
|
||||
// Get biometric credential ID for user
|
||||
export const getBiometricCredential = (username) => {
|
||||
const credentials = getBiometricCredentials();
|
||||
return credentials[username] || null;
|
||||
};
|
||||
|
||||
// Get all stored biometric credentials
|
||||
export const getBiometricCredentials = () => {
|
||||
const stored = localStorage.getItem("biometric_credentials");
|
||||
return stored ? JSON.parse(stored) : {};
|
||||
};
|
||||
|
||||
// Remove biometric credential
|
||||
export const removeBiometricCredential = (username) => {
|
||||
const credentials = getBiometricCredentials();
|
||||
delete credentials[username];
|
||||
localStorage.setItem("biometric_credentials", JSON.stringify(credentials));
|
||||
};
|
||||
|
||||
// Check if user has biometric registered
|
||||
export const hasBiometricRegistered = (username) => {
|
||||
return getBiometricCredential(username) !== null;
|
||||
};
|
||||
275
new-site/frontend/src/utils/chordEngine.js
Normal file
275
new-site/frontend/src/utils/chordEngine.js
Normal file
@@ -0,0 +1,275 @@
|
||||
// Chord Engine - Industry-standard chord transposition and parsing
|
||||
|
||||
const NOTES = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"];
|
||||
const FLAT_NOTES = [
|
||||
"C",
|
||||
"Db",
|
||||
"D",
|
||||
"Eb",
|
||||
"E",
|
||||
"F",
|
||||
"Gb",
|
||||
"G",
|
||||
"Ab",
|
||||
"A",
|
||||
"Bb",
|
||||
"B",
|
||||
];
|
||||
|
||||
// Keywords to ignore when parsing chords (section markers)
|
||||
const SECTION_KEYWORDS = [
|
||||
"verse",
|
||||
"chorus",
|
||||
"pre-chorus",
|
||||
"prechorus",
|
||||
"bridge",
|
||||
"intro",
|
||||
"outro",
|
||||
"tag",
|
||||
"coda",
|
||||
"interlude",
|
||||
"instrumental",
|
||||
"hook",
|
||||
"refrain",
|
||||
"vamp",
|
||||
"ending",
|
||||
"turnaround",
|
||||
"solo",
|
||||
"break",
|
||||
];
|
||||
|
||||
// Chord quality patterns
|
||||
const CHORD_QUALITIES = {
|
||||
major: "",
|
||||
minor: "m",
|
||||
diminished: "dim",
|
||||
augmented: "aug",
|
||||
suspended2: "sus2",
|
||||
suspended4: "sus4",
|
||||
dominant7: "7",
|
||||
major7: "maj7",
|
||||
minor7: "m7",
|
||||
diminished7: "dim7",
|
||||
augmented7: "aug7",
|
||||
add9: "add9",
|
||||
add11: "add11",
|
||||
6: "6",
|
||||
m6: "m6",
|
||||
9: "9",
|
||||
m9: "m9",
|
||||
11: "11",
|
||||
13: "13",
|
||||
};
|
||||
|
||||
// Regex to match chord patterns
|
||||
const CHORD_REGEX =
|
||||
/^([A-G][#b]?)(m|min|maj|dim|aug|sus[24]?|add[0-9]+|[0-9]+)?([0-9]*)?(\/[A-G][#b]?)?$/i;
|
||||
|
||||
/**
|
||||
* Parse a potential chord string
|
||||
* @param {string} text - Text that might be a chord
|
||||
* @returns {object|null} Parsed chord object or null if not a valid chord
|
||||
*/
|
||||
export function parseChord(text) {
|
||||
const trimmed = text.trim();
|
||||
|
||||
// Check if it's a section keyword
|
||||
if (
|
||||
SECTION_KEYWORDS.some((keyword) => trimmed.toLowerCase().includes(keyword))
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const match = trimmed.match(CHORD_REGEX);
|
||||
if (!match) return null;
|
||||
|
||||
const [, root, quality = "", extension = "", bass = ""] = match;
|
||||
|
||||
return {
|
||||
root: normalizeNote(root),
|
||||
quality,
|
||||
extension,
|
||||
bass: bass ? normalizeNote(bass.slice(1)) : null,
|
||||
original: trimmed,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize note name (handle enharmonics)
|
||||
* @param {string} note - Note name
|
||||
* @returns {string} Normalized note name
|
||||
*/
|
||||
function normalizeNote(note) {
|
||||
const upper = note.charAt(0).toUpperCase() + note.slice(1);
|
||||
// Convert flats to sharps for internal processing
|
||||
const flatIndex = FLAT_NOTES.indexOf(upper);
|
||||
if (flatIndex !== -1) {
|
||||
return NOTES[flatIndex];
|
||||
}
|
||||
return upper;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the semitone index of a note
|
||||
* @param {string} note - Note name
|
||||
* @returns {number} Semitone index (0-11)
|
||||
*/
|
||||
function getNoteIndex(note) {
|
||||
const normalized = normalizeNote(note);
|
||||
const index = NOTES.indexOf(normalized);
|
||||
return index !== -1 ? index : FLAT_NOTES.indexOf(normalized);
|
||||
}
|
||||
|
||||
/**
|
||||
* Transpose a single chord by a number of semitones
|
||||
* @param {string} chord - Chord string
|
||||
* @param {number} semitones - Number of semitones to transpose
|
||||
* @param {boolean} useFlats - Whether to use flats instead of sharps
|
||||
* @returns {string} Transposed chord string
|
||||
*/
|
||||
export function transposeChord(chord, semitones, useFlats = false) {
|
||||
const parsed = parseChord(chord);
|
||||
if (!parsed) return chord;
|
||||
|
||||
const noteArray = useFlats ? FLAT_NOTES : NOTES;
|
||||
|
||||
const rootIndex = getNoteIndex(parsed.root);
|
||||
const newRootIndex = (rootIndex + semitones + 12) % 12;
|
||||
const newRoot = noteArray[newRootIndex];
|
||||
|
||||
let result = newRoot + parsed.quality + parsed.extension;
|
||||
|
||||
if (parsed.bass) {
|
||||
const bassIndex = getNoteIndex(parsed.bass);
|
||||
const newBassIndex = (bassIndex + semitones + 12) % 12;
|
||||
result += "/" + noteArray[newBassIndex];
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get semitones between two keys
|
||||
* @param {string} fromKey - Starting key
|
||||
* @param {string} toKey - Target key
|
||||
* @returns {number} Number of semitones
|
||||
*/
|
||||
export function getSemitonesBetween(fromKey, toKey) {
|
||||
const fromIndex = getNoteIndex(fromKey);
|
||||
const toIndex = getNoteIndex(toKey);
|
||||
return (toIndex - fromIndex + 12) % 12;
|
||||
}
|
||||
|
||||
/**
|
||||
* Transpose all chords in a lyrics string
|
||||
* @param {string} lyrics - Lyrics with chords in brackets [C] or above words
|
||||
* @param {number} semitones - Number of semitones to transpose
|
||||
* @param {boolean} useFlats - Whether to use flats
|
||||
* @returns {string} Transposed lyrics
|
||||
*/
|
||||
export function transposeLyrics(lyrics, semitones, useFlats = false) {
|
||||
// Handle chords in brackets [C]
|
||||
const bracketPattern = /\[([^\]]+)\]/g;
|
||||
return lyrics.replace(bracketPattern, (match, chord) => {
|
||||
const transposed = transposeChord(chord, semitones, useFlats);
|
||||
return `[${transposed}]`;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract all unique chords from lyrics
|
||||
* @param {string} lyrics - Lyrics with chords
|
||||
* @returns {string[]} Array of unique chords
|
||||
*/
|
||||
export function extractChords(lyrics) {
|
||||
const bracketPattern = /\[([^\]]+)\]/g;
|
||||
const chords = new Set();
|
||||
let match;
|
||||
|
||||
while ((match = bracketPattern.exec(lyrics)) !== null) {
|
||||
const chord = match[1];
|
||||
if (parseChord(chord)) {
|
||||
chords.add(chord);
|
||||
}
|
||||
}
|
||||
|
||||
return Array.from(chords);
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect the likely key of a song based on its chords
|
||||
* @param {string[]} chords - Array of chords
|
||||
* @returns {string} Detected key
|
||||
*/
|
||||
export function detectKey(chords) {
|
||||
if (chords.length === 0) return "C";
|
||||
|
||||
// Simple heuristic: first and last chords often indicate the key
|
||||
const firstChord = parseChord(chords[0]);
|
||||
const lastChord = parseChord(chords[chords.length - 1]);
|
||||
|
||||
// If they match, high confidence
|
||||
if (firstChord && lastChord && firstChord.root === lastChord.root) {
|
||||
return firstChord.root + (firstChord.quality.includes("m") ? "m" : "");
|
||||
}
|
||||
|
||||
// Otherwise use first chord
|
||||
if (firstChord) {
|
||||
return firstChord.root + (firstChord.quality.includes("m") ? "m" : "");
|
||||
}
|
||||
|
||||
return "C";
|
||||
}
|
||||
|
||||
/**
|
||||
* Format chord for display (convert internal format to display format)
|
||||
* @param {string} chord - Chord in internal format
|
||||
* @param {object} options - Display options
|
||||
* @returns {string} Formatted chord
|
||||
*/
|
||||
export function formatChord(chord, options = {}) {
|
||||
const { useFlats = false, useSuperscript = false } = options;
|
||||
|
||||
let formatted = chord;
|
||||
|
||||
if (useFlats) {
|
||||
NOTES.forEach((sharp, i) => {
|
||||
if (sharp.includes("#")) {
|
||||
formatted = formatted.replace(sharp, FLAT_NOTES[i]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return formatted;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all keys for key selector
|
||||
* @param {boolean} useFlats - Whether to use flats
|
||||
* @returns {string[]} Array of keys
|
||||
*/
|
||||
export function getAllKeys(useFlats = false) {
|
||||
const notes = useFlats ? FLAT_NOTES : NOTES;
|
||||
const keys = [];
|
||||
|
||||
notes.forEach((note) => {
|
||||
keys.push(note); // Major
|
||||
keys.push(note + "m"); // Minor
|
||||
});
|
||||
|
||||
return keys;
|
||||
}
|
||||
|
||||
export default {
|
||||
parseChord,
|
||||
transposeChord,
|
||||
transposeLyrics,
|
||||
extractChords,
|
||||
detectKey,
|
||||
formatChord,
|
||||
getAllKeys,
|
||||
getSemitonesBetween,
|
||||
NOTES,
|
||||
FLAT_NOTES,
|
||||
SECTION_KEYWORDS,
|
||||
};
|
||||
778
new-site/frontend/src/utils/chordSheetUtils.js
Normal file
778
new-site/frontend/src/utils/chordSheetUtils.js
Normal file
@@ -0,0 +1,778 @@
|
||||
/**
|
||||
* Comprehensive Chord Sheet Utility
|
||||
* Uses ChordSheetJS for parsing/rendering and Tonal for music theory
|
||||
*/
|
||||
import ChordSheetJS from "chordsheetjs";
|
||||
import { Note, Scale, Chord, Progression } from "tonal";
|
||||
|
||||
// ============================================
|
||||
// COMPLETE CHORD TYPE DEFINITIONS
|
||||
// ============================================
|
||||
|
||||
// All root notes (chromatic scale)
|
||||
export const ROOT_NOTES = [
|
||||
"C",
|
||||
"C#",
|
||||
"Db",
|
||||
"D",
|
||||
"D#",
|
||||
"Eb",
|
||||
"E",
|
||||
"F",
|
||||
"F#",
|
||||
"Gb",
|
||||
"G",
|
||||
"G#",
|
||||
"Ab",
|
||||
"A",
|
||||
"A#",
|
||||
"Bb",
|
||||
"B",
|
||||
];
|
||||
|
||||
// All chord qualities/types
|
||||
export const CHORD_QUALITIES = [
|
||||
{ symbol: "", name: "Major" },
|
||||
{ symbol: "m", name: "Minor" },
|
||||
{ symbol: "dim", name: "Diminished" },
|
||||
{ symbol: "aug", name: "Augmented" },
|
||||
{ symbol: "7", name: "Dominant 7th" },
|
||||
{ symbol: "maj7", name: "Major 7th" },
|
||||
{ symbol: "m7", name: "Minor 7th" },
|
||||
{ symbol: "dim7", name: "Diminished 7th" },
|
||||
{ symbol: "m7b5", name: "Half-Diminished" },
|
||||
{ symbol: "aug7", name: "Augmented 7th" },
|
||||
{ symbol: "6", name: "Major 6th" },
|
||||
{ symbol: "m6", name: "Minor 6th" },
|
||||
{ symbol: "9", name: "Dominant 9th" },
|
||||
{ symbol: "maj9", name: "Major 9th" },
|
||||
{ symbol: "m9", name: "Minor 9th" },
|
||||
{ symbol: "11", name: "Dominant 11th" },
|
||||
{ symbol: "13", name: "Dominant 13th" },
|
||||
{ symbol: "sus2", name: "Suspended 2nd" },
|
||||
{ symbol: "sus4", name: "Suspended 4th" },
|
||||
{ symbol: "7sus4", name: "7th Suspended 4th" },
|
||||
{ symbol: "add9", name: "Add 9" },
|
||||
{ symbol: "add11", name: "Add 11" },
|
||||
{ symbol: "5", name: "Power Chord" },
|
||||
{ symbol: "2", name: "Add 2" },
|
||||
{ symbol: "4", name: "Add 4" },
|
||||
];
|
||||
|
||||
// Generate ALL possible chords (root + quality combinations)
|
||||
export const ALL_CHORDS = [];
|
||||
ROOT_NOTES.forEach((root) => {
|
||||
CHORD_QUALITIES.forEach((quality) => {
|
||||
ALL_CHORDS.push({
|
||||
chord: root + quality.symbol,
|
||||
root: root,
|
||||
quality: quality.symbol,
|
||||
name: `${root} ${quality.name}`,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// ============================================
|
||||
// KEY SIGNATURES WITH DIATONIC PROGRESSIONS
|
||||
// ============================================
|
||||
|
||||
// Major key diatonic chords (I, ii, iii, IV, V, vi, vii°)
|
||||
export const MAJOR_KEY_PROGRESSIONS = {
|
||||
C: ["C", "Dm", "Em", "F", "G", "Am", "Bdim"],
|
||||
"C#": ["C#", "D#m", "E#m", "F#", "G#", "A#m", "B#dim"],
|
||||
Db: ["Db", "Ebm", "Fm", "Gb", "Ab", "Bbm", "Cdim"],
|
||||
D: ["D", "Em", "F#m", "G", "A", "Bm", "C#dim"],
|
||||
"D#": ["D#", "E#m", "F##m", "G#", "A#", "B#m", "C##dim"],
|
||||
Eb: ["Eb", "Fm", "Gm", "Ab", "Bb", "Cm", "Ddim"],
|
||||
E: ["E", "F#m", "G#m", "A", "B", "C#m", "D#dim"],
|
||||
F: ["F", "Gm", "Am", "Bb", "C", "Dm", "Edim"],
|
||||
"F#": ["F#", "G#m", "A#m", "B", "C#", "D#m", "E#dim"],
|
||||
Gb: ["Gb", "Abm", "Bbm", "Cb", "Db", "Ebm", "Fdim"],
|
||||
G: ["G", "Am", "Bm", "C", "D", "Em", "F#dim"],
|
||||
"G#": ["G#", "A#m", "B#m", "C#", "D#", "E#m", "F##dim"],
|
||||
Ab: ["Ab", "Bbm", "Cm", "Db", "Eb", "Fm", "Gdim"],
|
||||
A: ["A", "Bm", "C#m", "D", "E", "F#m", "G#dim"],
|
||||
"A#": ["A#", "B#m", "C##m", "D#", "E#", "F##m", "G##dim"],
|
||||
Bb: ["Bb", "Cm", "Dm", "Eb", "F", "Gm", "Adim"],
|
||||
B: ["B", "C#m", "D#m", "E", "F#", "G#m", "A#dim"],
|
||||
};
|
||||
|
||||
// Minor key diatonic chords (i, ii°, III, iv, v, VI, VII)
|
||||
export const MINOR_KEY_PROGRESSIONS = {
|
||||
Am: ["Am", "Bdim", "C", "Dm", "Em", "F", "G"],
|
||||
"A#m": ["A#m", "B#dim", "C#", "D#m", "E#m", "F#", "G#"],
|
||||
Bbm: ["Bbm", "Cdim", "Db", "Ebm", "Fm", "Gb", "Ab"],
|
||||
Bm: ["Bm", "C#dim", "D", "Em", "F#m", "G", "A"],
|
||||
Cm: ["Cm", "Ddim", "Eb", "Fm", "Gm", "Ab", "Bb"],
|
||||
"C#m": ["C#m", "D#dim", "E", "F#m", "G#m", "A", "B"],
|
||||
Dm: ["Dm", "Edim", "F", "Gm", "Am", "Bb", "C"],
|
||||
"D#m": ["D#m", "E#dim", "F#", "G#m", "A#m", "B", "C#"],
|
||||
Ebm: ["Ebm", "Fdim", "Gb", "Abm", "Bbm", "Cb", "Db"],
|
||||
Em: ["Em", "F#dim", "G", "Am", "Bm", "C", "D"],
|
||||
Fm: ["Fm", "Gdim", "Ab", "Bbm", "Cm", "Db", "Eb"],
|
||||
"F#m": ["F#m", "G#dim", "A", "Bm", "C#m", "D", "E"],
|
||||
Gm: ["Gm", "Adim", "Bb", "Cm", "Dm", "Eb", "F"],
|
||||
"G#m": ["G#m", "A#dim", "B", "C#m", "D#m", "E", "F#"],
|
||||
};
|
||||
|
||||
// All keys for dropdown
|
||||
export const ALL_KEYS = [
|
||||
// Major keys
|
||||
{ value: "C", label: "C Major", type: "major" },
|
||||
{ value: "C#", label: "C# Major", type: "major" },
|
||||
{ value: "Db", label: "Db Major", type: "major" },
|
||||
{ value: "D", label: "D Major", type: "major" },
|
||||
{ value: "Eb", label: "Eb Major", type: "major" },
|
||||
{ value: "E", label: "E Major", type: "major" },
|
||||
{ value: "F", label: "F Major", type: "major" },
|
||||
{ value: "F#", label: "F# Major", type: "major" },
|
||||
{ value: "Gb", label: "Gb Major", type: "major" },
|
||||
{ value: "G", label: "G Major", type: "major" },
|
||||
{ value: "Ab", label: "Ab Major", type: "major" },
|
||||
{ value: "A", label: "A Major", type: "major" },
|
||||
{ value: "Bb", label: "Bb Major", type: "major" },
|
||||
{ value: "B", label: "B Major", type: "major" },
|
||||
// Minor keys
|
||||
{ value: "Am", label: "A Minor", type: "minor" },
|
||||
{ value: "A#m", label: "A# Minor", type: "minor" },
|
||||
{ value: "Bbm", label: "Bb Minor", type: "minor" },
|
||||
{ value: "Bm", label: "B Minor", type: "minor" },
|
||||
{ value: "Cm", label: "C Minor", type: "minor" },
|
||||
{ value: "C#m", label: "C# Minor", type: "minor" },
|
||||
{ value: "Dm", label: "D Minor", type: "minor" },
|
||||
{ value: "D#m", label: "D# Minor", type: "minor" },
|
||||
{ value: "Ebm", label: "Eb Minor", type: "minor" },
|
||||
{ value: "Em", label: "E Minor", type: "minor" },
|
||||
{ value: "Fm", label: "F Minor", type: "minor" },
|
||||
{ value: "F#m", label: "F# Minor", type: "minor" },
|
||||
{ value: "Gm", label: "G Minor", type: "minor" },
|
||||
{ value: "G#m", label: "G# Minor", type: "minor" },
|
||||
];
|
||||
|
||||
// ============================================
|
||||
// CHORD SHEET PARSING & RENDERING
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* Parse ChordPro format to structured song object
|
||||
* ChordPro format: [C]Amazing [G]grace how [Am]sweet
|
||||
*/
|
||||
export function parseChordPro(content) {
|
||||
const parser = new ChordSheetJS.ChordProParser();
|
||||
try {
|
||||
return parser.parse(content);
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse Ultimate Guitar / plain text format
|
||||
* Format with chords on separate lines above lyrics
|
||||
*/
|
||||
export function parsePlainText(content) {
|
||||
const parser = new ChordSheetJS.ChordsOverWordsParser();
|
||||
try {
|
||||
return parser.parse(content);
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render song to ChordPro format
|
||||
*/
|
||||
export function renderToChordPro(song) {
|
||||
const formatter = new ChordSheetJS.ChordProFormatter();
|
||||
return formatter.format(song);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render song to HTML with chords ABOVE lyrics
|
||||
*/
|
||||
export function renderToHtml(song) {
|
||||
const formatter = new ChordSheetJS.HtmlTableFormatter();
|
||||
return formatter.format(song);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render song to plain text with chords above lyrics
|
||||
*/
|
||||
export function renderToText(song) {
|
||||
const formatter = new ChordSheetJS.TextFormatter();
|
||||
return formatter.format(song);
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// TRANSPOSITION USING CHORDSHEETJS
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* Transpose a song by semitones
|
||||
*/
|
||||
export function transposeSong(song, semitones) {
|
||||
if (!song || semitones === 0) return song;
|
||||
return song.transpose(semitones);
|
||||
}
|
||||
|
||||
/**
|
||||
* Transpose from one key to another
|
||||
*/
|
||||
export function transposeToKey(song, fromKey, toKey) {
|
||||
const semitones = getSemitonesBetweenKeys(fromKey, toKey);
|
||||
return transposeSong(song, semitones);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate semitones between two keys
|
||||
*/
|
||||
export function getSemitonesBetweenKeys(fromKey, toKey) {
|
||||
const chromatic = [
|
||||
"C",
|
||||
"C#",
|
||||
"D",
|
||||
"D#",
|
||||
"E",
|
||||
"F",
|
||||
"F#",
|
||||
"G",
|
||||
"G#",
|
||||
"A",
|
||||
"A#",
|
||||
"B",
|
||||
];
|
||||
const flatToSharp = {
|
||||
Db: "C#",
|
||||
Eb: "D#",
|
||||
Fb: "E",
|
||||
Gb: "F#",
|
||||
Ab: "G#",
|
||||
Bb: "A#",
|
||||
Cb: "B",
|
||||
};
|
||||
|
||||
// Extract root note (remove minor suffix if present)
|
||||
const fromRoot = fromKey.replace(/m$/, "");
|
||||
const toRoot = toKey.replace(/m$/, "");
|
||||
|
||||
// Normalize to sharp notation
|
||||
const fromNorm = flatToSharp[fromRoot] || fromRoot;
|
||||
const toNorm = flatToSharp[toRoot] || toRoot;
|
||||
|
||||
const fromIdx = chromatic.indexOf(fromNorm);
|
||||
const toIdx = chromatic.indexOf(toNorm);
|
||||
|
||||
if (fromIdx === -1 || toIdx === -1) return 0;
|
||||
|
||||
return (toIdx - fromIdx + 12) % 12;
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// CHORD PROGRESSION GENERATION
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* Get diatonic chords for a key
|
||||
*/
|
||||
export function getDiatonicChords(key) {
|
||||
const isMinor = key.endsWith("m");
|
||||
|
||||
if (isMinor) {
|
||||
return MINOR_KEY_PROGRESSIONS[key] || MINOR_KEY_PROGRESSIONS["Am"];
|
||||
}
|
||||
return MAJOR_KEY_PROGRESSIONS[key] || MAJOR_KEY_PROGRESSIONS["C"];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get common chord progressions for a key
|
||||
*/
|
||||
export function getCommonProgressions(key) {
|
||||
const chords = getDiatonicChords(key);
|
||||
const isMinor = key.endsWith("m");
|
||||
|
||||
// Roman numeral positions
|
||||
// Major: I=0, ii=1, iii=2, IV=3, V=4, vi=5, vii°=6
|
||||
// Minor: i=0, ii°=1, III=2, iv=3, v=4, VI=5, VII=6
|
||||
|
||||
return {
|
||||
"I-IV-V-I": [chords[0], chords[3], chords[4], chords[0]],
|
||||
"I-V-vi-IV": [chords[0], chords[4], chords[5], chords[3]],
|
||||
"I-vi-IV-V": [chords[0], chords[5], chords[3], chords[4]],
|
||||
"ii-V-I": [chords[1], chords[4], chords[0]],
|
||||
"I-IV-vi-V": [chords[0], chords[3], chords[5], chords[4]],
|
||||
"vi-IV-I-V": [chords[5], chords[3], chords[0], chords[4]],
|
||||
"I-ii-IV-V": [chords[0], chords[1], chords[3], chords[4]],
|
||||
"I-iii-IV-V": [chords[0], chords[2], chords[3], chords[4]],
|
||||
};
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// LYRICS + CHORD POSITIONING
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* Parse lyrics with embedded [Chord] markers OR chords-above-lyrics format
|
||||
* for rendering chords ABOVE lyrics
|
||||
*
|
||||
* Input format: [C]Amazing [G]grace how [Am]sweet
|
||||
* OR:
|
||||
* Am F G
|
||||
* Lord prepare me
|
||||
*
|
||||
* Output: Array of Array of { chord: string|null, text: string }
|
||||
*/
|
||||
export function parseLyricsWithChords(lyrics) {
|
||||
if (!lyrics) return [];
|
||||
|
||||
const lines = lyrics.split("\n");
|
||||
const result = [];
|
||||
|
||||
// Pattern to detect if a line is ONLY chords (chords-above-lyrics format)
|
||||
const chordOnlyPattern =
|
||||
/^[\sA-G#b/()m\d]*[A-G][#b]?(?:m|maj|min|dim|aug|sus|add|2|4|5|6|7|9|11|13)*[\sA-G#b/()m\d]*$/;
|
||||
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i];
|
||||
const nextLine = lines[i + 1];
|
||||
|
||||
// Check if this line is a chord line (contains only chords and spaces)
|
||||
const isChordLine =
|
||||
line.trim().length > 0 &&
|
||||
chordOnlyPattern.test(line.trim()) &&
|
||||
nextLine !== undefined &&
|
||||
!/^[A-G][#b]?(?:m|maj|min|dim|aug|sus|add)?[0-9]*[\s]*$/.test(
|
||||
nextLine.trim(),
|
||||
);
|
||||
|
||||
if (isChordLine && nextLine) {
|
||||
// This is chords-above-lyrics format
|
||||
// Parse chord positions and merge with lyrics
|
||||
const segments = [];
|
||||
const chordRegex = /([A-G][#b]?(?:m|maj|min|dim|aug|sus|add)?[0-9]*)/g;
|
||||
const chords = [];
|
||||
let match;
|
||||
|
||||
while ((match = chordRegex.exec(line)) !== null) {
|
||||
chords.push({
|
||||
chord: match[1],
|
||||
position: match.index,
|
||||
});
|
||||
}
|
||||
|
||||
if (chords.length > 0) {
|
||||
let lastPos = 0;
|
||||
|
||||
chords.forEach((chordObj, idx) => {
|
||||
const { chord, position } = chordObj;
|
||||
const textBefore = nextLine.substring(lastPos, position);
|
||||
|
||||
if (textBefore) {
|
||||
segments.push({ chord: null, text: textBefore });
|
||||
}
|
||||
|
||||
// Get text after this chord until next chord or end
|
||||
const nextChordPos = chords[idx + 1]
|
||||
? chords[idx + 1].position
|
||||
: nextLine.length;
|
||||
const textAfter = nextLine.substring(position, nextChordPos);
|
||||
|
||||
segments.push({
|
||||
chord: chord,
|
||||
text: textAfter || " ",
|
||||
});
|
||||
|
||||
lastPos = nextChordPos;
|
||||
});
|
||||
|
||||
// Add any remaining text
|
||||
if (lastPos < nextLine.length) {
|
||||
segments.push({ chord: null, text: nextLine.substring(lastPos) });
|
||||
}
|
||||
|
||||
result.push(segments);
|
||||
i++; // Skip the lyrics line since we processed it
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Check for embedded [Chord] format
|
||||
const hasEmbeddedChords =
|
||||
/\[([A-Ga-g][#b]?(?:m|M|maj|min|dim|aug|sus|add|2|4|5|6|7|9|11|13)*(?:\/[A-Ga-g][#b]?)?)\]/.test(
|
||||
line,
|
||||
);
|
||||
|
||||
if (hasEmbeddedChords) {
|
||||
// Parse embedded [Chord] markers
|
||||
const segments = [];
|
||||
const regex =
|
||||
/\[([A-Ga-g][#b]?(?:m|M|maj|min|dim|aug|sus|add|2|4|5|6|7|9|11|13)*(?:\/[A-Ga-g][#b]?)?)\]/g;
|
||||
|
||||
let lastIndex = 0;
|
||||
let chordMatch;
|
||||
|
||||
while ((chordMatch = regex.exec(line)) !== null) {
|
||||
// Text before this chord (no chord above it)
|
||||
if (chordMatch.index > lastIndex) {
|
||||
const textBefore = line.substring(lastIndex, chordMatch.index);
|
||||
if (textBefore) {
|
||||
segments.push({ chord: null, text: textBefore });
|
||||
}
|
||||
}
|
||||
|
||||
// Find text after chord until next chord or end
|
||||
const afterChordIdx = chordMatch.index + chordMatch[0].length;
|
||||
const nextMatch = regex.exec(line);
|
||||
const nextIdx = nextMatch ? nextMatch.index : line.length;
|
||||
regex.lastIndex = afterChordIdx; // Reset to continue from after chord
|
||||
|
||||
const textAfter = line.substring(afterChordIdx, nextIdx);
|
||||
segments.push({
|
||||
chord: chordMatch[1],
|
||||
text: textAfter || " ",
|
||||
});
|
||||
|
||||
lastIndex = nextIdx;
|
||||
|
||||
// If we peeked ahead, go back
|
||||
if (nextMatch) {
|
||||
regex.lastIndex = nextMatch.index;
|
||||
}
|
||||
}
|
||||
|
||||
// Remaining text
|
||||
if (lastIndex < line.length) {
|
||||
segments.push({ chord: null, text: line.substring(lastIndex) });
|
||||
}
|
||||
|
||||
// If no chords found, whole line is plain text
|
||||
if (segments.length === 0) {
|
||||
segments.push({ chord: null, text: line });
|
||||
}
|
||||
|
||||
result.push(segments);
|
||||
} else {
|
||||
// Plain text line (no chords)
|
||||
result.push([{ chord: null, text: line }]);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Transpose chord markers in lyrics text - handles BOTH formats:
|
||||
* 1. Embedded [Chord] format
|
||||
* 2. Chords-above-lyrics format (chord-only lines)
|
||||
*/
|
||||
export function transposeLyricsText(lyrics, fromKey, toKey) {
|
||||
if (!lyrics || fromKey === toKey) return lyrics;
|
||||
|
||||
const semitones = getSemitonesBetweenKeys(fromKey, toKey);
|
||||
if (semitones === 0) return lyrics;
|
||||
|
||||
// Use flat notation for flat keys
|
||||
const flatKeys = [
|
||||
"F",
|
||||
"Bb",
|
||||
"Eb",
|
||||
"Ab",
|
||||
"Db",
|
||||
"Gb",
|
||||
"Dm",
|
||||
"Gm",
|
||||
"Cm",
|
||||
"Fm",
|
||||
"Bbm",
|
||||
"Ebm",
|
||||
];
|
||||
const useFlats = flatKeys.includes(toKey);
|
||||
|
||||
// Split into lines to handle both formats
|
||||
const lines = lyrics.split("\n");
|
||||
const chordOnlyPattern =
|
||||
/^[\sA-G#b/()m\d]*[A-G][#b]?(?:m|maj|min|dim|aug|sus|add|2|4|5|6|7|9|11|13)*[\sA-G#b/()m\d]*$/;
|
||||
|
||||
const transposedLines = lines.map((line) => {
|
||||
// Check if this is a chord-only line (chords-above-lyrics format)
|
||||
if (line.trim().length > 0 && chordOnlyPattern.test(line.trim())) {
|
||||
// Transpose all standalone chords in this line
|
||||
return line.replace(
|
||||
/([A-G][#b]?(?:m|maj|min|dim|aug|sus|add)?[0-9]*)/g,
|
||||
(match) => {
|
||||
return transposeChordName(match, semitones, useFlats);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// Otherwise, transpose embedded [Chord] patterns
|
||||
return line.replace(
|
||||
/\[([A-Ga-g][#b]?(?:m|M|maj|min|dim|aug|sus|add|2|4|5|6|7|9|11|13)*(?:\/[A-Ga-g][#b]?)?)\]/g,
|
||||
(match, chord) => {
|
||||
const transposed = transposeChordName(chord, semitones, useFlats);
|
||||
return `[${transposed}]`;
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
return transposedLines.join("\n");
|
||||
}
|
||||
|
||||
/**
|
||||
* Transpose a single chord name by semitones
|
||||
*/
|
||||
export function transposeChordName(chord, semitones, useFlats = false) {
|
||||
if (!chord || semitones === 0) return chord;
|
||||
|
||||
// Handle slash chords (e.g., C/G)
|
||||
if (chord.includes("/")) {
|
||||
const [main, bass] = chord.split("/");
|
||||
return (
|
||||
transposeChordName(main, semitones, useFlats) +
|
||||
"/" +
|
||||
transposeChordName(bass, semitones, useFlats)
|
||||
);
|
||||
}
|
||||
|
||||
// Parse root and quality
|
||||
const match = chord.match(/^([A-Ga-g][#b]?)(.*)$/);
|
||||
if (!match) return chord;
|
||||
|
||||
const [, root, quality] = match;
|
||||
|
||||
const sharpScale = [
|
||||
"C",
|
||||
"C#",
|
||||
"D",
|
||||
"D#",
|
||||
"E",
|
||||
"F",
|
||||
"F#",
|
||||
"G",
|
||||
"G#",
|
||||
"A",
|
||||
"A#",
|
||||
"B",
|
||||
];
|
||||
const flatScale = [
|
||||
"C",
|
||||
"Db",
|
||||
"D",
|
||||
"Eb",
|
||||
"E",
|
||||
"F",
|
||||
"Gb",
|
||||
"G",
|
||||
"Ab",
|
||||
"A",
|
||||
"Bb",
|
||||
"B",
|
||||
];
|
||||
const flatToSharp = {
|
||||
Db: "C#",
|
||||
Eb: "D#",
|
||||
Fb: "E",
|
||||
Gb: "F#",
|
||||
Ab: "G#",
|
||||
Bb: "A#",
|
||||
Cb: "B",
|
||||
};
|
||||
|
||||
// Normalize root to sharp
|
||||
const normRoot = root[0].toUpperCase() + (root.slice(1) || "");
|
||||
const sharpRoot = flatToSharp[normRoot] || normRoot;
|
||||
|
||||
// Find index
|
||||
let idx = sharpScale.indexOf(sharpRoot);
|
||||
if (idx === -1) return chord;
|
||||
|
||||
// Transpose
|
||||
const newIdx = (idx + semitones + 12) % 12;
|
||||
const scale = useFlats ? flatScale : sharpScale;
|
||||
const newRoot = scale[newIdx];
|
||||
|
||||
return newRoot + quality;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect the original key from lyrics content
|
||||
*/
|
||||
export function detectKeyFromLyrics(lyrics) {
|
||||
if (!lyrics) return "C";
|
||||
|
||||
// Find all chords in the lyrics
|
||||
const chordMatches = lyrics.match(
|
||||
/\[([A-Ga-g][#b]?(?:m|M|maj|min|dim|aug|sus|add|2|4|5|6|7|9|11|13)*)\]/g,
|
||||
);
|
||||
if (!chordMatches || chordMatches.length === 0) return "C";
|
||||
|
||||
// Extract just the chord names
|
||||
const chords = chordMatches.map((m) => m.replace(/[\[\]]/g, ""));
|
||||
|
||||
// The first chord is often the key
|
||||
const firstChord = chords[0];
|
||||
|
||||
// Check if it's minor
|
||||
if (firstChord.includes("m") && !firstChord.includes("maj")) {
|
||||
return firstChord.match(/^[A-Ga-g][#b]?m/)?.[0] || "Am";
|
||||
}
|
||||
|
||||
// Return root as major key
|
||||
return firstChord.match(/^[A-Ga-g][#b]?/)?.[0] || "C";
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert chord markers into plain lyrics at specified positions
|
||||
*/
|
||||
export function insertChordsIntoLyrics(lyrics, chordPositions) {
|
||||
// chordPositions: Array of { line: number, position: number, chord: string }
|
||||
if (!lyrics || !chordPositions || chordPositions.length === 0) return lyrics;
|
||||
|
||||
const lines = lyrics.split("\n");
|
||||
|
||||
// Group by line
|
||||
const byLine = {};
|
||||
chordPositions.forEach((cp) => {
|
||||
if (!byLine[cp.line]) byLine[cp.line] = [];
|
||||
byLine[cp.line].push(cp);
|
||||
});
|
||||
|
||||
// Process each line
|
||||
return lines
|
||||
.map((line, lineIdx) => {
|
||||
const lineChords = byLine[lineIdx];
|
||||
if (!lineChords || lineChords.length === 0) return line;
|
||||
|
||||
// Sort by position descending to insert from end
|
||||
lineChords.sort((a, b) => b.position - a.position);
|
||||
|
||||
let result = line;
|
||||
lineChords.forEach((cp) => {
|
||||
const pos = Math.min(cp.position, result.length);
|
||||
result = result.slice(0, pos) + `[${cp.chord}]` + result.slice(pos);
|
||||
});
|
||||
|
||||
return result;
|
||||
})
|
||||
.join("\n");
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert ChordsOverWords format to ChordPro inline format
|
||||
*/
|
||||
export function convertChordsOverWordsToInline(content) {
|
||||
if (!content) return content;
|
||||
|
||||
const lines = content.split("\n");
|
||||
const result = [];
|
||||
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i];
|
||||
const nextLine = lines[i + 1];
|
||||
|
||||
// Check if current line is mostly chords
|
||||
if (isChordLine(line) && nextLine && !isChordLine(nextLine)) {
|
||||
// Merge chord line with lyrics line
|
||||
const merged = mergeChordLineWithLyrics(line, nextLine);
|
||||
result.push(merged);
|
||||
i++; // Skip the next line
|
||||
} else if (!isChordLine(line)) {
|
||||
result.push(line);
|
||||
}
|
||||
}
|
||||
|
||||
return result.join("\n");
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a line is primarily chord notation
|
||||
*/
|
||||
function isChordLine(line) {
|
||||
if (!line || line.trim().length === 0) return false;
|
||||
|
||||
// Remove chord patterns and see what's left
|
||||
const withoutChords = line.replace(
|
||||
/[A-Ga-g][#b]?(?:m|M|maj|min|dim|aug|sus|add|2|4|5|6|7|9|11|13)*/g,
|
||||
"",
|
||||
);
|
||||
const originalLength = line.replace(/\s/g, "").length;
|
||||
const remainingLength = withoutChords.replace(/\s/g, "").length;
|
||||
|
||||
// If most content was chords, it's a chord line
|
||||
return remainingLength < originalLength * 0.3;
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge a chord line with the lyrics line below it
|
||||
*/
|
||||
function mergeChordLineWithLyrics(chordLine, lyricsLine) {
|
||||
const chordPositions = [];
|
||||
|
||||
// Find all chords and their positions
|
||||
const regex =
|
||||
/([A-Ga-g][#b]?(?:m|M|maj|min|dim|aug|sus|add|2|4|5|6|7|9|11|13)*(?:\/[A-Ga-g][#b]?)?)/g;
|
||||
let match;
|
||||
|
||||
while ((match = regex.exec(chordLine)) !== null) {
|
||||
chordPositions.push({
|
||||
chord: match[1],
|
||||
position: match.index,
|
||||
});
|
||||
}
|
||||
|
||||
// Sort by position
|
||||
chordPositions.sort((a, b) => a.position - b.position);
|
||||
|
||||
// Insert chords into lyrics at corresponding positions
|
||||
let result = "";
|
||||
let lastPos = 0;
|
||||
|
||||
for (const { chord, position } of chordPositions) {
|
||||
const lyricsPos = Math.min(position, lyricsLine.length);
|
||||
result += lyricsLine.substring(lastPos, lyricsPos) + `[${chord}]`;
|
||||
lastPos = lyricsPos;
|
||||
}
|
||||
|
||||
result += lyricsLine.substring(lastPos);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// EXPORT DEFAULT
|
||||
// ============================================
|
||||
|
||||
export default {
|
||||
// Constants
|
||||
ROOT_NOTES,
|
||||
CHORD_QUALITIES,
|
||||
ALL_CHORDS,
|
||||
ALL_KEYS,
|
||||
MAJOR_KEY_PROGRESSIONS,
|
||||
MINOR_KEY_PROGRESSIONS,
|
||||
|
||||
// Parsing
|
||||
parseChordPro,
|
||||
parsePlainText,
|
||||
parseLyricsWithChords,
|
||||
|
||||
// Rendering
|
||||
renderToChordPro,
|
||||
renderToHtml,
|
||||
renderToText,
|
||||
|
||||
// Transposition
|
||||
transposeSong,
|
||||
transposeToKey,
|
||||
transposeLyricsText,
|
||||
transposeChordName,
|
||||
getSemitonesBetweenKeys,
|
||||
|
||||
// Progressions
|
||||
getDiatonicChords,
|
||||
getCommonProgressions,
|
||||
|
||||
// Utilities
|
||||
detectKeyFromLyrics,
|
||||
insertChordsIntoLyrics,
|
||||
convertChordsOverWordsToInline,
|
||||
};
|
||||
371
new-site/frontend/src/utils/chordUtils.js
Normal file
371
new-site/frontend/src/utils/chordUtils.js
Normal file
@@ -0,0 +1,371 @@
|
||||
/**
|
||||
* Chord Transposition Utility
|
||||
* Uses tonal.js for accurate music theory-based transposition
|
||||
*/
|
||||
import { Note, Interval, Chord } from "tonal";
|
||||
|
||||
// All chromatic notes for transposition
|
||||
const CHROMATIC_SCALE = [
|
||||
"C",
|
||||
"C#",
|
||||
"D",
|
||||
"D#",
|
||||
"E",
|
||||
"F",
|
||||
"F#",
|
||||
"G",
|
||||
"G#",
|
||||
"A",
|
||||
"A#",
|
||||
"B",
|
||||
];
|
||||
const CHROMATIC_FLATS = [
|
||||
"C",
|
||||
"Db",
|
||||
"D",
|
||||
"Eb",
|
||||
"E",
|
||||
"F",
|
||||
"Gb",
|
||||
"G",
|
||||
"Ab",
|
||||
"A",
|
||||
"Bb",
|
||||
"B",
|
||||
];
|
||||
|
||||
// Common key signatures (for UI display)
|
||||
export const KEY_OPTIONS = [
|
||||
{ value: "C", label: "C Major" },
|
||||
{ value: "C#", label: "C# / Db Major" },
|
||||
{ value: "D", label: "D Major" },
|
||||
{ value: "D#", label: "D# / Eb Major" },
|
||||
{ value: "E", label: "E Major" },
|
||||
{ value: "F", label: "F Major" },
|
||||
{ value: "F#", label: "F# / Gb Major" },
|
||||
{ value: "G", label: "G Major" },
|
||||
{ value: "G#", label: "G# / Ab Major" },
|
||||
{ value: "A", label: "A Major" },
|
||||
{ value: "A#", label: "A# / Bb Major" },
|
||||
{ value: "B", label: "B Major" },
|
||||
{ value: "Cm", label: "C Minor" },
|
||||
{ value: "Dm", label: "D Minor" },
|
||||
{ value: "Em", label: "E Minor" },
|
||||
{ value: "Fm", label: "F Minor" },
|
||||
{ value: "Gm", label: "G Minor" },
|
||||
{ value: "Am", label: "A Minor" },
|
||||
{ value: "Bm", label: "B Minor" },
|
||||
];
|
||||
|
||||
/**
|
||||
* Get the semitone value of a note (0-11)
|
||||
*/
|
||||
function getNoteIndex(note) {
|
||||
const normalized = note.replace(/b/g, "").replace(/#/g, "");
|
||||
let index = CHROMATIC_SCALE.indexOf(normalized.toUpperCase());
|
||||
|
||||
// Handle sharps
|
||||
if (note.includes("#")) {
|
||||
index = (index + 1) % 12;
|
||||
}
|
||||
// Handle flats
|
||||
if (note.includes("b") || note.includes("♭")) {
|
||||
index = (index - 1 + 12) % 12;
|
||||
}
|
||||
|
||||
return index;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate semitone difference between two keys
|
||||
*/
|
||||
export function getSemitoneDistance(fromKey, toKey) {
|
||||
const fromRoot = fromKey.replace(/m$/, ""); // Remove minor suffix
|
||||
const toRoot = toKey.replace(/m$/, "");
|
||||
|
||||
const fromIndex = getNoteIndex(fromRoot);
|
||||
const toIndex = getNoteIndex(toRoot);
|
||||
|
||||
return (toIndex - fromIndex + 12) % 12;
|
||||
}
|
||||
|
||||
/**
|
||||
* Transpose a single chord by semitones
|
||||
* @param {string} chord - The chord to transpose (e.g., "Am7", "F#m", "Cmaj7")
|
||||
* @param {number} semitones - Number of semitones to transpose
|
||||
* @param {boolean} useFlats - Whether to use flats instead of sharps
|
||||
* @returns {string} - The transposed chord
|
||||
*/
|
||||
export function transposeChord(chord, semitones, useFlats = false) {
|
||||
if (!chord || semitones === 0) return chord;
|
||||
|
||||
// Parse the chord to extract root and quality
|
||||
const match = chord.match(/^([A-Ga-g][#b♯♭]?)(.*)$/);
|
||||
if (!match) return chord;
|
||||
|
||||
const [, root, quality] = match;
|
||||
|
||||
// Get current note index
|
||||
const currentIndex = getNoteIndex(root);
|
||||
|
||||
// Calculate new index
|
||||
const newIndex = (currentIndex + semitones + 12) % 12;
|
||||
|
||||
// Get new root note
|
||||
const scale = useFlats ? CHROMATIC_FLATS : CHROMATIC_SCALE;
|
||||
const newRoot = scale[newIndex];
|
||||
|
||||
// Preserve original case
|
||||
const finalRoot =
|
||||
root === root.toLowerCase() ? newRoot.toLowerCase() : newRoot;
|
||||
|
||||
return finalRoot + quality;
|
||||
}
|
||||
|
||||
/**
|
||||
* Transpose all chords in a string
|
||||
* @param {string} text - Text containing chords in brackets or standalone
|
||||
* @param {number} semitones - Semitones to transpose
|
||||
* @param {boolean} useFlats - Use flats instead of sharps
|
||||
*/
|
||||
export function transposeText(text, semitones, useFlats = false) {
|
||||
if (!text || semitones === 0) return text;
|
||||
|
||||
// Match chords in various formats: [Am7], (Cmaj7), or standalone chord patterns
|
||||
const chordPattern =
|
||||
/(\[?)([A-G][#b♯♭]?(?:m|maj|min|dim|aug|sus|add)?[0-9]?(?:\/[A-G][#b♯♭]?)?)(\]?)/g;
|
||||
|
||||
return text.replace(chordPattern, (match, open, chord, close) => {
|
||||
// Handle slash chords (e.g., C/G)
|
||||
if (chord.includes("/")) {
|
||||
const [main, bass] = chord.split("/");
|
||||
const transposedMain = transposeChord(main, semitones, useFlats);
|
||||
const transposedBass = transposeChord(bass, semitones, useFlats);
|
||||
return `${open}${transposedMain}/${transposedBass}${close}`;
|
||||
}
|
||||
return `${open}${transposeChord(chord, semitones, useFlats)}${close}`;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse lyrics with inline chords
|
||||
* Chord format: [Chord] before the word/syllable it applies to
|
||||
* Example: "[Am]Amazing [G]grace how [D]sweet the [Am]sound"
|
||||
*
|
||||
* @param {string} lyrics - Raw lyrics with chords
|
||||
* @returns {Array} - Array of lines, each containing segments with chord/text pairs
|
||||
*/
|
||||
export function parseLyricsWithChords(lyrics) {
|
||||
if (!lyrics) return [];
|
||||
|
||||
const lines = lyrics.split("\n");
|
||||
|
||||
return lines.map((line) => {
|
||||
const segments = [];
|
||||
let currentPosition = 0;
|
||||
|
||||
// Match chord patterns like [Am], [G7], [F#m], etc.
|
||||
const chordRegex =
|
||||
/\[([A-G][#b♯♭]?(?:m|maj|min|dim|aug|sus|add|M)?[0-9]*(?:\/[A-G][#b♯♭]?)?)\]/g;
|
||||
let match;
|
||||
|
||||
while ((match = chordRegex.exec(line)) !== null) {
|
||||
// Add text before this chord (if any)
|
||||
if (match.index > currentPosition) {
|
||||
const textBefore = line.substring(currentPosition, match.index);
|
||||
if (textBefore) {
|
||||
// This text has no chord above it
|
||||
segments.push({ chord: null, text: textBefore });
|
||||
}
|
||||
}
|
||||
|
||||
// Find the text after the chord until the next chord or end of line
|
||||
const chordEnd = match.index + match[0].length;
|
||||
const nextChordMatch = line.substring(chordEnd).match(/\[[A-G]/);
|
||||
const nextChordIndex = nextChordMatch
|
||||
? chordEnd + nextChordMatch.index
|
||||
: line.length;
|
||||
|
||||
const textAfter = line.substring(chordEnd, nextChordIndex);
|
||||
|
||||
segments.push({
|
||||
chord: match[1],
|
||||
text: textAfter || " ", // At least a space for positioning
|
||||
});
|
||||
|
||||
currentPosition = nextChordIndex;
|
||||
}
|
||||
|
||||
// Add remaining text without chord
|
||||
if (currentPosition < line.length) {
|
||||
const remaining = line.substring(currentPosition);
|
||||
if (remaining) {
|
||||
segments.push({ chord: null, text: remaining });
|
||||
}
|
||||
}
|
||||
|
||||
// If no chords found, the whole line is text
|
||||
if (segments.length === 0) {
|
||||
segments.push({ chord: null, text: line });
|
||||
}
|
||||
|
||||
return segments;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert plain lyrics with chord lines to inline format
|
||||
* Handles the common format where chords are on their own line above lyrics
|
||||
*
|
||||
* Example input:
|
||||
* Am G D
|
||||
* Amazing grace how sweet
|
||||
*
|
||||
* Output:
|
||||
* [Am]Amazing [G]grace how [D]sweet
|
||||
*/
|
||||
export function convertChordLinestoInline(lyrics) {
|
||||
if (!lyrics) return lyrics;
|
||||
|
||||
const lines = lyrics.split("\n");
|
||||
const result = [];
|
||||
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i];
|
||||
const nextLine = lines[i + 1];
|
||||
|
||||
// Check if this line is a chord line (contains mostly chord patterns)
|
||||
if (isChordLine(line) && nextLine && !isChordLine(nextLine)) {
|
||||
// Merge chord line with lyric line below
|
||||
const merged = mergeChordAndLyricLines(line, nextLine);
|
||||
result.push(merged);
|
||||
i++; // Skip the lyric line since we merged it
|
||||
} else if (!isChordLine(line)) {
|
||||
// Regular lyric or section header
|
||||
result.push(line);
|
||||
}
|
||||
// Skip standalone chord lines without lyrics below
|
||||
}
|
||||
|
||||
return result.join("\n");
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a line contains primarily chords
|
||||
*/
|
||||
function isChordLine(line) {
|
||||
if (!line || line.trim().length === 0) return false;
|
||||
|
||||
// Remove all chord patterns and see what's left
|
||||
const withoutChords = line
|
||||
.replace(
|
||||
/[A-G][#b♯♭]?(?:m|maj|min|dim|aug|sus|add|M)?[0-9]*(?:\/[A-G][#b♯♭]?)?/g,
|
||||
"",
|
||||
)
|
||||
.trim();
|
||||
const originalContent = line.replace(/\s+/g, "");
|
||||
|
||||
// If removing chords leaves very little, it's a chord line
|
||||
return withoutChords.length < originalContent.length * 0.3;
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge a chord line with the lyric line below it
|
||||
*/
|
||||
function mergeChordAndLyricLines(chordLine, lyricLine) {
|
||||
const chordPositions = [];
|
||||
|
||||
// Find all chords and their positions
|
||||
const chordRegex =
|
||||
/([A-G][#b♯♭]?(?:m|maj|min|dim|aug|sus|add|M)?[0-9]*(?:\/[A-G][#b♯♭]?)?)/g;
|
||||
let match;
|
||||
|
||||
while ((match = chordRegex.exec(chordLine)) !== null) {
|
||||
chordPositions.push({
|
||||
chord: match[1],
|
||||
position: match.index,
|
||||
});
|
||||
}
|
||||
|
||||
// Sort by position (in case regex finds them out of order)
|
||||
chordPositions.sort((a, b) => a.position - b.position);
|
||||
|
||||
// Build the merged line by inserting chords into the lyric
|
||||
let result = "";
|
||||
let lastPos = 0;
|
||||
|
||||
for (const { chord, position } of chordPositions) {
|
||||
// Add lyrics up to this chord position
|
||||
const lyricsBefore = lyricLine.substring(
|
||||
lastPos,
|
||||
Math.min(position, lyricLine.length),
|
||||
);
|
||||
result += lyricsBefore + `[${chord}]`;
|
||||
lastPos = Math.min(position, lyricLine.length);
|
||||
}
|
||||
|
||||
// Add remaining lyrics
|
||||
result += lyricLine.substring(lastPos);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the original key from song data
|
||||
*/
|
||||
export function extractOriginalKey(song) {
|
||||
if (!song) return "C";
|
||||
|
||||
// Check various possible fields
|
||||
if (song.key_chord)
|
||||
return song.key_chord.replace(/\s+/g, "").split(/[,\/]/)[0];
|
||||
if (song.chords) return song.chords.replace(/\s+/g, "").split(/[,\/\s]/)[0];
|
||||
if (song.original_key) return song.original_key;
|
||||
if (song.key) return song.key;
|
||||
|
||||
// Try to detect from lyrics
|
||||
const lyrics = song.lyrics || "";
|
||||
const firstChord = lyrics.match(/\[([A-G][#b♯♭]?m?)\]/);
|
||||
if (firstChord) return firstChord[1];
|
||||
|
||||
return "C"; // Default
|
||||
}
|
||||
|
||||
/**
|
||||
* Transpose entire lyrics to a new key
|
||||
*/
|
||||
export function transposeLyrics(lyrics, fromKey, toKey) {
|
||||
if (!lyrics || fromKey === toKey) return lyrics;
|
||||
|
||||
const semitones = getSemitoneDistance(fromKey, toKey);
|
||||
|
||||
// Determine if we should use flats based on the target key
|
||||
const flatKeys = [
|
||||
"F",
|
||||
"Bb",
|
||||
"Eb",
|
||||
"Ab",
|
||||
"Db",
|
||||
"Gb",
|
||||
"Dm",
|
||||
"Gm",
|
||||
"Cm",
|
||||
"Fm",
|
||||
"Bbm",
|
||||
"Ebm",
|
||||
];
|
||||
const useFlats = flatKeys.includes(toKey);
|
||||
|
||||
return transposeText(lyrics, semitones, useFlats);
|
||||
}
|
||||
|
||||
export default {
|
||||
KEY_OPTIONS,
|
||||
transposeChord,
|
||||
transposeText,
|
||||
parseLyricsWithChords,
|
||||
convertChordLinestoInline,
|
||||
getSemitoneDistance,
|
||||
transposeLyrics,
|
||||
extractOriginalKey,
|
||||
};
|
||||
29
new-site/frontend/src/utils/debounce.js
Normal file
29
new-site/frontend/src/utils/debounce.js
Normal file
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* Creates a debounced function that delays invoking func until after wait milliseconds
|
||||
* have elapsed since the last time the debounced function was invoked.
|
||||
*/
|
||||
export function debounce(func, wait = 300) {
|
||||
let timeoutId = null;
|
||||
|
||||
const debounced = (...args) => {
|
||||
if (timeoutId) {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
|
||||
timeoutId = setTimeout(() => {
|
||||
func.apply(this, args);
|
||||
timeoutId = null;
|
||||
}, wait);
|
||||
};
|
||||
|
||||
debounced.cancel = () => {
|
||||
if (timeoutId) {
|
||||
clearTimeout(timeoutId);
|
||||
timeoutId = null;
|
||||
}
|
||||
};
|
||||
|
||||
return debounced;
|
||||
}
|
||||
|
||||
export default debounce;
|
||||
165
new-site/frontend/src/utils/documentParser.js
Normal file
165
new-site/frontend/src/utils/documentParser.js
Normal file
@@ -0,0 +1,165 @@
|
||||
/**
|
||||
* Parse chord sheet from plain text
|
||||
* Detects chords above lyrics and section headers
|
||||
*/
|
||||
export function parseChordSheet(text) {
|
||||
const lines = text.split("\n");
|
||||
const result = {
|
||||
title: "",
|
||||
artist: "",
|
||||
key: "",
|
||||
sections: [],
|
||||
chords: new Set(),
|
||||
lyrics: "",
|
||||
};
|
||||
|
||||
let currentSection = { type: "", lines: [] };
|
||||
let lyricsLines = [];
|
||||
|
||||
// Common section headers
|
||||
const sectionPattern =
|
||||
/^\s*[\[\(]?(verse|chorus|bridge|pre-?chorus|intro|outro|interlude|hook|tag|ending|v\d|c\d|ch\d)[\d\s]*[\]\)]?\s*:?\s*$/i;
|
||||
|
||||
// Chord pattern - detects chords on a line
|
||||
const chordLinePattern = /^[\s\w#b/]+$/;
|
||||
const chordPattern = /([A-G][#b]?(?:m|maj|min|dim|aug|sus|add)?[0-9]*)/g;
|
||||
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i];
|
||||
const trimmed = line.trim();
|
||||
|
||||
// Skip empty lines
|
||||
if (!trimmed) {
|
||||
lyricsLines.push("");
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check for section headers
|
||||
if (sectionPattern.test(trimmed)) {
|
||||
if (currentSection.lines.length > 0) {
|
||||
result.sections.push({ ...currentSection });
|
||||
}
|
||||
currentSection = {
|
||||
type: trimmed.replace(/[\[\]\(\):]/g, "").trim(),
|
||||
lines: [],
|
||||
};
|
||||
lyricsLines.push(trimmed);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if line contains only chords
|
||||
const nextLine = i + 1 < lines.length ? lines[i + 1] : "";
|
||||
const isChordLine =
|
||||
chordLinePattern.test(trimmed) &&
|
||||
trimmed.match(chordPattern) &&
|
||||
nextLine.trim() &&
|
||||
!chordLinePattern.test(nextLine.trim());
|
||||
|
||||
if (isChordLine) {
|
||||
// Extract chords from this line
|
||||
const chords = trimmed.match(chordPattern);
|
||||
if (chords) {
|
||||
chords.forEach((chord) => result.chords.add(chord));
|
||||
}
|
||||
|
||||
// Map chords to positions in the next line
|
||||
const lyricLine = nextLine;
|
||||
let chordedLine = "";
|
||||
let lastPos = 0;
|
||||
|
||||
// Find chord positions
|
||||
const chordPositions = [];
|
||||
let tempLine = trimmed;
|
||||
let pos = 0;
|
||||
|
||||
for (const chord of trimmed.split(/\s+/).filter((c) => c.trim())) {
|
||||
const chordMatch = chord.match(chordPattern);
|
||||
if (chordMatch) {
|
||||
const chordPos = trimmed.indexOf(chord, pos);
|
||||
chordPositions.push({ chord, position: chordPos });
|
||||
pos = chordPos + chord.length;
|
||||
}
|
||||
}
|
||||
|
||||
// Build lyrics with embedded chords
|
||||
chordPositions.forEach((item, idx) => {
|
||||
const { chord, position } = item;
|
||||
const nextPos = chordPositions[idx + 1]?.position || lyricLine.length;
|
||||
const lyricPart = lyricLine.substring(position, nextPos).trim();
|
||||
|
||||
if (lyricPart) {
|
||||
chordedLine += `[${chord}]${lyricPart} `;
|
||||
}
|
||||
});
|
||||
|
||||
lyricsLines.push(chordedLine.trim() || `[${chords[0]}]`);
|
||||
currentSection.lines.push(chordedLine.trim());
|
||||
|
||||
// Skip the next line since we processed it
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Regular lyric line
|
||||
lyricsLines.push(line);
|
||||
currentSection.lines.push(line);
|
||||
}
|
||||
|
||||
if (currentSection.lines.length > 0) {
|
||||
result.sections.push(currentSection);
|
||||
}
|
||||
|
||||
result.lyrics = lyricsLines.join("\n");
|
||||
result.chords = Array.from(result.chords).join(" ");
|
||||
|
||||
// Try to detect key from first chord
|
||||
if (result.chords) {
|
||||
const firstChord = result.chords.split(" ")[0];
|
||||
result.key = firstChord;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse Word document (.docx) using mammoth
|
||||
*/
|
||||
export async function parseWordDocument(file) {
|
||||
// For now, read as text and parse
|
||||
const text = await file.text();
|
||||
return parseChordSheet(text);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse PDF document
|
||||
*/
|
||||
export async function parsePDFDocument(file) {
|
||||
// For now, read as text and parse
|
||||
const text = await file.text();
|
||||
return parseChordSheet(text);
|
||||
}
|
||||
|
||||
/**
|
||||
* Auto-detect file type and parse
|
||||
*/
|
||||
export async function parseDocument(file) {
|
||||
const fileType = file.type;
|
||||
const fileName = file.name.toLowerCase();
|
||||
|
||||
if (fileType === "application/pdf" || fileName.endsWith(".pdf")) {
|
||||
return parsePDFDocument(file);
|
||||
} else if (
|
||||
fileType ===
|
||||
"application/vnd.openxmlformats-officedocument.wordprocessingml.document" ||
|
||||
fileName.endsWith(".docx")
|
||||
) {
|
||||
return parseWordDocument(file);
|
||||
} else if (fileType === "text/plain" || fileName.endsWith(".txt")) {
|
||||
const text = await file.text();
|
||||
return parseChordSheet(text);
|
||||
} else {
|
||||
throw new Error(
|
||||
"Unsupported file type. Please upload PDF, Word, or TXT files.",
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user