// Public: A module that takes a string representing the BMS notechart, // parses it, and compiles into a {BMSChart}. /* module */ import { match } from '../util/match' import { BMSChart } from '../bms/chart' import { BMSObject } from '../bms/objects' const matchers = { bms: { random: /^#RANDOM\s+(\d+)$/i, if: /^#IF\s+(\d+)$/i, endif: /^#ENDIF$/i, timeSignature: /^#(\d\d\d)02:(\S*)$/, channel: /^#(?:EXT\s+#)?(\d\d\d)(\S\S):(\S*)$/, header: /^#(\w+)(?:\s+(\S.*))?$/, }, dtx: { random: /^#RANDOM\s+(\d+)$/i, if: /^#IF\s+(\d+)$/i, endif: /^#ENDIF$/i, timeSignature: /^#(\d\d\d)02:\s*(\S*)$/, channel: /^#(?:EXT\s+#)?(\d\d\d)(\S\S):\s*(\S*)$/, header: /^#(\w+):(?:\s+(\S.*))?$/, }, } /** * Reads the string representing the BMS notechart, parses it, * and compiles into a {BMSChart}. * @param text the BMS notechart * @param options additional parser options */ export function compile(text: string, options?: Partial) { options = options || {} const chart = new BMSChart() const rng = options.rng || function (max) { return 1 + Math.floor(Math.random() * max) } const matcher = (options.format && matchers[options.format]) || matchers.bms const randomStack: number[] = [] const skipStack = [false] const result = { headerSentences: 0, channelSentences: 0, controlSentences: 0, skippedSentences: 0, malformedSentences: 0, /** * The resulting chart */ chart: chart, /** * Warnings found during compilation */ warnings: [] as { lineNumber: number; message: string }[], } eachLine(text, function (text, lineNumber) { let flow = true if (text.charAt(0) !== '#') return match(text) .when(matcher.random, function (m) { result.controlSentences += 1 randomStack.push(rng(+m[1])) }) .when(matcher.if, function (m) { result.controlSentences += 1 skipStack.push(randomStack[randomStack.length - 1] !== +m[1]) }) .when(matcher.endif, function (m) { result.controlSentences += 1 skipStack.pop() }) .else(function () { flow = false }) if (flow) return const skipped = skipStack[skipStack.length - 1] match(text) .when(matcher.timeSignature, function (m) { result.channelSentences += 1 if (!skipped) chart.timeSignatures.set(+m[1], +m[2]) }) .when(matcher.channel, function (m) { result.channelSentences += 1 if (!skipped) handleChannelSentence(+m[1], m[2], m[3], lineNumber) }) .when(matcher.header, function (m) { result.headerSentences += 1 if (!skipped) chart.headers.set(m[1], m[2]) }) .else(function () { warn(lineNumber, 'Invalid command') }) }) return result function handleChannelSentence( measure: number, channel: string, string: string, lineNumber: number ) { const items = Math.floor(string.length / 2) if (items === 0) return for (let i = 0; i < items; i++) { const value = string.substr(i * 2, 2) const fraction = i / items if (value === '00') continue chart.objects.add({ measure: measure, fraction: fraction, value: value, channel: channel, lineNumber: lineNumber, } as BMSObject) } } function warn(lineNumber: number, message: string) { result.warnings.push({ lineNumber: lineNumber, message: message, }) } } function eachLine( text: string, callback: (line: string, index: number) => void ) { text .split(/\r\n|\r|\n/) .map(function (line) { return line.trim() }) .forEach(function (line, index) { callback(line, index + 1) }) } export interface BMSCompileOptions { /** File format */ format: 'bms' | 'dtx' /** A function that generates a random number. * It is used when processing `#RANDOM n` directive. * This function should return an integer number between 1 and `n`. */ rng: (max: number) => number }