UNPKG

@fs-eire/wgsl-template

Version:

A powerful template system for generating WGSL (WebGPU Shading Language) code with support for parameters, conditionals, and multiple output formats including C++ code generation.

234 lines 11.8 kB
import { WgslTemplateParseError } from "./errors.js"; /** * Parses raw content of a template file and remove comments. * * This function removes both single-line and multi-line comments * while preserving the original line structure. Empty lines after comment removal * are preserved to maintain line numbers for error reporting. */ function parseComments(filePath, raw) { const rawWithoutComments = []; let inMultiLineComment = false; for (const line of raw) { let processedLine = ""; let i = 0; while (i < line.length) { if (inMultiLineComment) { // Look for end of multi-line comment if (i < line.length - 1 && line[i] === "*" && line[i + 1] === "/") { inMultiLineComment = false; i += 2; // Skip '*/' } else { i++; } } else { // Check for start of single-line comment if (i < line.length - 1 && line[i] === "/" && line[i + 1] === "/") { // Rest of line is a comment, stop processing break; } // Check for start of multi-line comment else if (i < line.length - 1 && line[i] === "/" && line[i + 1] === "*") { inMultiLineComment = true; i += 2; // Skip '/*' } else { // Regular character, add to processed line processedLine += line[i]; i++; } } } // Always add the line (even if empty) to preserve line numbers rawWithoutComments.push(processedLine.trimEnd()); } if (inMultiLineComment) { throw new WgslTemplateParseError("Unterminated multi-line comment detected in template", "comment-removal", { filePath, }); } return rawWithoutComments; } /** * Parses #include preprocessor directives. * * @param includeStack represents the stack of currently processed includes * @param parseState a map that stores the state of each file being parsed */ function parsePreprocessorIncludeDirectives(includeStack, parseState) { const lines = []; const currentFile = includeStack[includeStack.length - 1]; const currentState = parseState.get(currentFile); if (!currentState) { throw new WgslTemplateParseError(`File "${currentFile}" not found in parse state`, "include-resolution", { filePath: currentFile, }); } if (currentState.includeProcessed) { // If the current file has already been processed, return its lines return; } for (let lineNumber = 0; lineNumber < currentState.lines.length; lineNumber++) { const parsedLine = currentState.lines[lineNumber]; const line = parsedLine.line; // Process each line and extract include directives const includeMatch = line.match(/^#include\s+(.+)$/); if (includeMatch) { const includeParam = includeMatch[1].trim(); if (!(includeParam.startsWith('"') && includeParam.endsWith('"'))) { throw new WgslTemplateParseError(`Invalid #include directive in file ${currentFile} at line ${lineNumber + 1}: file path must be enclosed in double quotes`, "syntax-error", { filePath: currentFile, lineNumber: lineNumber + 1 }); } const includePath = includeParam.slice(1, -1); // Remove quotes if (includeStack.includes(includePath)) { throw new WgslTemplateParseError(`Circular #include detected in file ${currentFile} at line ${lineNumber + 1}: ${includePath} is already included`, "include-circular", { filePath: currentFile, lineNumber: lineNumber + 1 }); } if (!parseState.has(includePath)) { throw new WgslTemplateParseError(`File "${includePath}" not found in parse state for #include directive in file "${currentFile}" at line ${lineNumber + 1}`, "include-not-found", { filePath: currentFile, lineNumber: lineNumber + 1 }); } includeStack.push(includePath); parsePreprocessorIncludeDirectives(includeStack, parseState); lines.push(...parseState.get(includePath).lines); includeStack.pop(); } else { // If no include directive, just add the original line to the result // (preserve comments in the output) lines.push(parsedLine); } } currentState.includeProcessed = true; currentState.lines = lines; } /** * Parses #define macro directives and applies macro substitutions. * * @param lines Array of lines to process * @param fileName Name of the file being processed (for error reporting) * @returns Array of lines with macros defined and substituted */ function parseMacroDirectives(lines, fileName) { const macros = new Map(); const processedLines = []; for (let lineNumber = 0; lineNumber < lines.length; lineNumber++) { const parsedLine = lines[lineNumber]; let line = parsedLine.line; // Check for malformed #define directives if (line.trim().startsWith("#define ")) { const defineMatch = line.match(/^#define\s+([a-zA-Z_][a-zA-Z0-9_]*)\s+(.+)$/); if (!defineMatch) { // Check specific error cases const emptyValueMatch = line.match(/^#define\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*$/); if (emptyValueMatch) { throw new WgslTemplateParseError(`Invalid macro definition in file ${fileName} at line ${lineNumber + 1}: macro "${emptyValueMatch[1]}" has no value`, "syntax-error", { filePath: fileName, lineNumber: lineNumber + 1 }); } const invalidNameMatch = line.match(/^#define\s+(\S+)(?:\s+(.+))?$/); if (invalidNameMatch) { throw new WgslTemplateParseError(`Invalid macro definition in file ${fileName} at line ${lineNumber + 1}: invalid macro name "${invalidNameMatch[1]}" (must start with letter or underscore, contain only letters, numbers, and underscores)`, "syntax-error", { filePath: fileName, lineNumber: lineNumber + 1 }); } throw new WgslTemplateParseError(`Invalid macro definition in file ${fileName} at line ${lineNumber + 1}: malformed #define directive`, "syntax-error", { filePath: fileName, lineNumber: lineNumber + 1 }); } // Valid #define directive const macroName = defineMatch[1]; const macroValue = defineMatch[2].trim(); // Check for whitespace-only value if (macroValue === "") { throw new WgslTemplateParseError(`Invalid macro definition in file ${fileName} at line ${lineNumber + 1}: macro "${macroName}" has empty value (whitespace only)`, "syntax-error", { filePath: fileName, lineNumber: lineNumber + 1 }); } // Check for duplicate macro definition if (macros.has(macroName)) { throw new WgslTemplateParseError(`Duplicate macro definition in file ${fileName} at line ${lineNumber + 1}: "${macroName}" is already defined`, "define-expansion", { filePath: fileName, lineNumber: lineNumber + 1 }); } // Check for direct circular reference (macro referencing itself) const directCircularRegex = new RegExp(`\\b${macroName}\\b`); if (directCircularRegex.test(macroValue)) { throw new WgslTemplateParseError(`Circular macro reference in file ${fileName} at line ${lineNumber + 1}: macro "${macroName}" references itself`, "define-expansion", { filePath: fileName, lineNumber: lineNumber + 1 }); } // Apply existing macro substitutions to the new macro value // Track which macros we're expanding to detect circular references let expandedValue = macroValue; for (const [existingMacroName, existingMacroValue] of macros) { const regex = new RegExp(`\\b${existingMacroName}\\b`, "g"); if (regex.test(expandedValue)) { // Check if the existing macro value contains the current macro name (circular reference) const circularRegex = new RegExp(`\\b${macroName}\\b`); if (circularRegex.test(existingMacroValue)) { throw new WgslTemplateParseError(`Circular macro reference in file ${fileName} at line ${lineNumber + 1}: macro "${macroName}" creates circular dependency with "${existingMacroName}"`, "define-expansion", { filePath: fileName, lineNumber: lineNumber + 1 }); } expandedValue = expandedValue.replace(regex, existingMacroValue); } } macros.set(macroName, expandedValue); // Clear the line since it's a macro definition line = ""; } else { // Apply macro substitutions to the current line for (const [macroName, macroValue] of macros) { // Use word boundaries to ensure we only replace whole identifiers const regex = new RegExp(`\\b${macroName}\\b`, "g"); line = line.replace(regex, macroValue); } } processedLines.push({ line, codeReference: parsedLine.codeReference, }); } return processedLines; } /** * TemplateParser is an implementation of the Parser interface that parses * WGSL template files and converts raw content into structured segments. * It identifies preprocessor directives, comments, and raw text segments. */ export const parser = { /** * Parses a template file and converts its raw content into structured segments. * * @param filePath The path to the template file to parse * @param repo The repository containing raw template data * @returns A new repository with parsed segments added to the specified template */ parse(repo) { const pass1Repo = new Map(); const parseState = new Map(); // STEP.1. Parse comments. for (const [templateKey, template] of repo.templates) { const parsedLines = parseComments(templateKey, template.raw); parseState.set(templateKey, { lines: parsedLines.map((line, index) => ({ line, codeReference: { filePath: templateKey, lineNumber: index + 1, // Line numbers are 1-based }, })), includeProcessed: false, }); } // STEP.2. Parse #include preprocessor directives. for (const [templateKey, template] of repo.templates) { parsePreprocessorIncludeDirectives([templateKey], parseState); pass1Repo.set(templateKey, { filePath: template.filePath, raw: template.raw, pass1: parseState.get(templateKey).lines, }); } // STEP.3. Parse #define macro directives and apply substitutions. for (const [templateKey, template] of pass1Repo) { const processedLines = parseMacroDirectives([...template.pass1], templateKey); pass1Repo.set(templateKey, { filePath: template.filePath, raw: template.raw, pass1: processedLines, }); } return { basePath: repo.basePath, templates: pass1Repo, }; }, }; //# sourceMappingURL=parser-impl.js.map