UNPKG

@devaloop/prettier-plugin-devalang

Version:

Prettier plugin for Devalang formatting

484 lines (401 loc) 13.4 kB
import { Doc, AstPath, doc } from "prettier"; import { ProgramNode, BlockContext, BlankLine, BpmDeclaration, BankDeclaration, LoopBlock, TriggerCall, GroupBlock, CallStatement, SpawnStatement, IfStatement, ObjectProperty } from "./interfaces/statement"; import { print } from "./printer"; import { DurationValue } from "./types/duration"; import { Expression } from "./types/expression"; import { Node } from "./types/node"; const { hardline } = doc.builders; /** * Parses a value string into an Expression. * @param value string * @returns Expression */ const parseValue = (value: string): Expression => { if (!value) { return { type: "ObjectLiteral", properties: [] }; } value = value.trim(); if (value === "true") { return { type: "BooleanLiteral", value: true }; } if (value === "false") { return { type: "BooleanLiteral", value: false }; } if (value === "auto") { return { type: "Identifier", name: "auto" }; } if (/^\d+(\.\d+)?$/.test(value)) { return { type: "NumberLiteral", value: parseFloat(value) }; } if (/^".*"$/.test(value)) { return { type: "StringLiteral", value: value.slice(1, -1) }; } if (value.startsWith("synth ")) { return { type: "SynthReference", name: value.slice(6).trim() }; } const objectMatch = value.match(/^\{(.*)\}$/); if (objectMatch) { const body = objectMatch[1].trim(); if (body === "") { return { type: "ObjectLiteral", properties: [] }; } const props: ObjectProperty[] = body .split(",") .map((pair) => pair.trim()) .filter((pair) => pair.includes(":")) .map((pair) => { const [k, v] = pair.split(":").map((s) => s.trim()); return { type: "ObjectProperty", key: k, value: parseValue(v), }; }); return { type: "ObjectLiteral", properties: props, }; } return { type: "Identifier", name: value }; }; /** * Parses a block of nodes * @param block Node[] * @returns Doc[] */ export const parseBlock = (block: Node[]): Doc[] => { const parts: Doc[] = []; let i = 0; for (const node of block) { const printed = print({ getValue: () => node } as AstPath<Node>, {}, () => ""); if (printed !== "" && printed !== undefined) { parts.push(printed); if (i !== block.length - 1) { parts.push(hardline); } } i++; } if (parts[parts.length - 1] === hardline) { parts.pop(); // deletes trailing hardline } return parts; } /** * Parses a Devalang program from a string into an AST. * @param text string * @returns ProgramNode */ export const parse = (text: string): ProgramNode => { const lines = text.split("\n"); const body: Node[] = []; const stack: (BlockContext & { bodyIndent: number | null })[] = []; let lastNonBlankLineIndex: number | null = null; function pushToBodyOrBlock(indent: number, node: Node) { for (let j = stack.length - 1; j >= 0; j--) { const parent = stack[j]; if ('body' in parent.node) { if (parent.bodyIndent === null && node.type !== "BlankLine") { parent.bodyIndent = indent; } if (parent.bodyIndent !== null && indent >= parent.bodyIndent) { parent.node.body.push(node); return; } } } body.push(node); } for (let i = 0; i < lines.length; i++) { const line = lines[i]; const indent = line.search(/\S|$/); const trimmedLine = line.trim(); if (trimmedLine === "") { const blank: BlankLine = { type: "BlankLine" }; pushToBodyOrBlock(indent, blank); continue; } lastNonBlankLineIndex = i; if (trimmedLine.startsWith("#")) { pushToBodyOrBlock(indent, { type: "Comment", value: trimmedLine }); i++; continue; } if (trimmedLine.startsWith("bpm ")) { const decl: BpmDeclaration = { type: "BpmDeclaration", identifier: trimmedLine.split(" ")[1] }; pushToBodyOrBlock(indent, decl); i++; continue; } if (trimmedLine.startsWith("bank ")) { const decl: BankDeclaration = { type: "BankDeclaration", identifier: trimmedLine.split(" ")[1] }; pushToBodyOrBlock(indent, decl); i++; continue; } if (trimmedLine.startsWith("let ")) { const [, name, , ...rest] = trimmedLine.split(" "); const rawValue = rest.join(" "); const value = parseValue(rawValue); pushToBodyOrBlock(indent, { type: "LetDeclaration", name, value }); i++; continue; } // IF const ifMatch = trimmedLine.match(/^if\s+(.+):$/); if (ifMatch) { const ifBody: Node[] = []; const ifNode: IfStatement = { type: "If", condition: ifMatch[1].trim(), body: ifBody, elseIfs: [], }; pushToBodyOrBlock(indent, ifNode); stack.push({ type: "If", indent, node: ifNode, bodyIndent: null }); continue; } const elseIfMatch = trimmedLine.match(/^else if\s+(.+):$/); if (elseIfMatch) console.log("Matched ELSE IF!", elseIfMatch); if (elseIfMatch) { const parentIf = stack .slice() .reverse() .find((s) => s.type === "If")?.node as IfStatement | undefined; if (parentIf) { const block: Node[] = []; parentIf.elseIfs.push({ condition: elseIfMatch[1].trim(), body: block }); pushToBodyOrBlock(indent, { type: "BlankLine" }); stack.push({ type: "ElseIf", indent, node: { body: block }, bodyIndent: null }); continue; } } const elseMatch = trimmedLine.match(/^else\s*:\s*$/); if (elseMatch) { const parentIf = stack .slice() .reverse() .find((s) => s.type === "If")?.node as IfStatement | undefined; if (parentIf) { parentIf.alternate = []; pushToBodyOrBlock(indent, { type: "BlankLine" }); stack.push({ type: "Else", indent, node: { body: parentIf.alternate }, bodyIndent: null }); continue; } } const loopMatch = trimmedLine.match(/^loop\s+([^\s:]+)\s*:/); if (loopMatch) { const rawIterator = parseValue(loopMatch[1]); const loop: LoopBlock = { type: "Loop", iterator: { type: rawIterator.type === "Identifier" ? "Identifier" : "NumberLiteral", value: rawIterator.type === "NumberLiteral" ? rawIterator.value : rawIterator.type === "Identifier" ? rawIterator.name : 0 }, body: [] }; pushToBodyOrBlock(indent, loop); stack.push({ type: "Loop", indent, node: loop, bodyIndent: null }); continue; } const triggerMatch = trimmedLine.match(/^\.(\S+)(?:\s+(.*))?$/); if (triggerMatch) { const [, name, rawArgsInitial] = triggerMatch; let rawArgs = rawArgsInitial ?? ""; let braceDepth = 0; for (const char of rawArgs) { if (char === "{") braceDepth++; if (char === "}") braceDepth--; } let j = i + 1; while (braceDepth > 0 && j < lines.length) { const nextLine = lines[j].trim(); rawArgs += " " + nextLine; for (const char of nextLine) { if (char === "{") braceDepth++; if (char === "}") braceDepth--; } j++; } i = j - 1; const parts = splitArguments(rawArgs); let duration: DurationValue | undefined = undefined; const parsedArgs: Expression[] = []; for (const part of parts) { if (/^\{.*\}$/.test(part)) { const expr = parseValue(part); if (expr.type === "ObjectLiteral") { for (const prop of expr.properties) { if (prop.key === "duration") { const val = prop.value; if (val.type === "NumberLiteral") { duration = { type: "Milliseconds", value: val.value }; } else if ( (val.type === "StringLiteral" && /^\d+\/\d+$/.test(val.value)) || (val.type === "Identifier" && /^\d+\/\d+$/.test(val.name)) ) { duration = { type: "BeatDuration", value: val.type === "StringLiteral" ? val.value : val.name, }; } } else { parsedArgs.push({ type: "ObjectProperty", key: prop.key, value: prop.value, }); } } } else if (expr.type === "Identifier") { parsedArgs.push({ type: "Identifier", name: expr.name }); } } else if (/^\d+\/\d+$/.test(part)) { if (!duration) { duration = { type: "BeatDuration", value: part }; } } else if (/^\d+(\.\d+)?$/.test(part)) { if (!duration) { duration = { type: "Milliseconds", value: parseFloat(part) }; } } else if (/^auto$/i.test(part)) { if (!duration) { duration = { type: "AutoDuration", value: "auto" }; } } else { parsedArgs.push({ type: "Identifier", name: part }); } } const trigger: TriggerCall = { type: "Trigger", name: `.${name}`, args: parsedArgs, ...(duration ? { duration } : {}) }; pushToBodyOrBlock(indent, trigger); continue; } const importMatch = trimmedLine.match(/^@import\s*\{\s*([^}]+)\s*\}\s*from\s*"([^"]+)"/); if (importMatch) { const [, idList, fromPath] = importMatch; const identifiers = idList.split(",").map((s) => s.trim()); pushToBodyOrBlock(indent, { type: "ImportStatement", identifiers, from: fromPath }); i++; continue; } const exportMatch = trimmedLine.match(/^@export\s*\{\s*([^}]+)\s*\}/); if (exportMatch) { const [, idList] = exportMatch; const identifiers = idList.split(",").map((s) => s.trim()); pushToBodyOrBlock(indent, { type: "ExportStatement", identifiers }); i++; continue; } const loadMatch = trimmedLine.match(/^@load\s+"([^"]+)"\s+as\s+([a-zA-Z_][\w]*)/); if (loadMatch) { const [, path, alias] = loadMatch; pushToBodyOrBlock(indent, { type: "LoadSample", path, alias }); i++; continue; } const groupMatch = trimmedLine.match(/^group\s+([a-zA-Z_][\w]*)\s*:/); if (groupMatch) { const group: GroupBlock = { type: "Group", name: groupMatch[1], body: [] }; body.push(group); stack.push({ type: "Group", indent, node: group, bodyIndent: null }); i++; continue; } const callMatch = trimmedLine.match(/^call\s+([a-zA-Z_][\w]*)$/); if (callMatch) { const call: CallStatement = { type: "Call", identifier: callMatch[1] }; pushToBodyOrBlock(indent, call); i++; continue; } const spawnMatch = trimmedLine.match(/^spawn\s+([a-zA-Z_][\w]*)$/); if (spawnMatch) { const spawn: SpawnStatement = { type: "Spawn", identifier: spawnMatch[1] }; pushToBodyOrBlock(indent, spawn); i++; continue; } const sleepMatch = trimmedLine.match(/^sleep\s+(.*)$/); if (sleepMatch) { const expr = parseValue(sleepMatch[1]); pushToBodyOrBlock(indent, { type: "Sleep", value: expr }); i++; continue; } const arrowCallMatch = trimmedLine.match(/^([a-zA-Z_][\w]*)\s*->\s*([a-zA-Z_][\w]*)\((.*)\)$/); if (arrowCallMatch) { const [, target, method, argsStr] = arrowCallMatch; const args = splitArguments(argsStr).map(parseValue); pushToBodyOrBlock(indent, { type: "ArrowCall", target, method, args }); i++; continue; } while ( stack.length > 0 && indent < (stack[stack.length - 1].bodyIndent ?? stack[stack.length - 1].indent) ) { stack.pop(); } pushToBodyOrBlock(indent, { type: "Unknown", value: trimmedLine }); continue; } return { type: "Program", body }; }; const splitArguments = (argStr: string): string[] => { const args: string[] = []; let current = ""; let depth = 0; let inString = false; for (let i = 0; i < argStr.length; i++) { const char = argStr[i]; if (char === '"' && argStr[i - 1] !== '\\') { inString = !inString; } if (!inString) { if (char === '{') depth++; if (char === '}') depth--; } const isSplitter = (char === " " || char === ",") && depth === 0 && !inString; if (isSplitter) { if (current.trim()) args.push(current.trim()); current = ""; } else { current += char; } } if (current.trim()) args.push(current.trim()); return args; }; export const astFormat = "devalang";