UNPKG

@bscotch/stitch

Version:

Stitch: The GameMaker Studio 2 Asset Pipeline Development Kit.

209 lines 8.5 kB
import XRegExp from 'xregexp'; import { assert, StitchError } from '../../utility/errors.js'; import { GmlToken } from './GmlToken.js'; import { GmlTokenLocation } from './GmlTokenLocation.js'; import { GmlTokenVersioned } from './GmlTokenVersioned.js'; const functionNameRegex = /\bfunction\s*(?<name>[a-zA-Z_][a-zA-Z0-9_]+)/; function countMatches(source, pattern) { return source.match(new RegExp(pattern, 'g'))?.length || 0; } /** * Replace a portion of a string, starting at `start`, * and subsequent `length` chars with a different string */ export function stringReplaceSegment(string, start, length) { const leftSide = string.slice(0, start); const rightSide = string.slice(start + length); const middle = string .slice(start, start + length) .split('') .map((char) => (char == '\n' ? char : ' ')) .join(''); return `${leftSide}${middle}${rightSide}`; } /** * Return a copy of the source GML with all comments and * strings replaced with whitespace, preserving the ovderall * structure of the GML but preventing such content from * confusing other parser activities (stuff inside strings * and comments can use keywords or variable names without * creating references -- further, string and comment delimiters * can confuse *each other*, so clearing all of that is helpful.) */ export function stripCommentsAndStringsFromGml(gml) { // Start of either a string or comment let stripped = gml; const leftDelimiterNames = [ 'commentMulti', 'commentSingle', 'quoteMulti', 'quoteSingle', ]; const leftDelimiterRegex = /((?<commentMulti>(?<!\/)\/\*)|(?<commentSingle>\/\/.*)|(?<quoteMulti>@["'])|(?<quoteSingle>"))/g; const _findRightPosition = (endRegex, startFrom) => { assert(endRegex.global, 'End-regex must be global'); endRegex.lastIndex = startFrom; const rightMatch = endRegex.exec(stripped); return rightMatch ? rightMatch.index + rightMatch[0].length : stripped.length; }; const found = { strings: [], comments: [], }; while (true) { const leftDelimiterMatch = leftDelimiterRegex.exec(stripped); if (!leftDelimiterMatch) { break; } const leftDelimiterGroups = leftDelimiterMatch.groups; const left = leftDelimiterMatch.index; const leftText = leftDelimiterMatch[1]; let right = left + leftText.length; let type = 'strings'; let replaceLeftOffset = 0; let replaceRightOffset = 0; // Move the start/end based on what type of thing we got. for (const delimiterName of leftDelimiterNames) { if (!leftDelimiterGroups[delimiterName]) { // Not this one! continue; } // Based on the delimiter find the index of the end of what it controls if (delimiterName == 'commentSingle') { // Then we already have it (goes to end of line)! type = 'comments'; } else if (delimiterName == 'quoteSingle') { // Move the start point to AFTER the quote, and end to BEFORE endquote // Need to get the end quote but skip ESCAPED quotes const endRegex = /"/g; endRegex.lastIndex = right; while (true) { const match = endRegex.exec(stripped); assert(match, 'Found a left quote without a matching right quote.'); right = match.index + 1; //We have found a right quote, but we don't know if it is escaped by having an odd number of forward slashes in front of it let consecutivePreviousSlashCount = 0; for (let position = match.index - 1; position > left; position--) { if (stripped[position] == '\\') { consecutivePreviousSlashCount++; } else { break; } } if (consecutivePreviousSlashCount % 2 == 1) { continue; } else { break; } } replaceLeftOffset += 1; replaceRightOffset -= 1; } else if (delimiterName == 'commentMulti') { // Find the end delimiter const endRegex = /\*\//g; right = _findRightPosition(endRegex, right); type = 'comments'; } else if (delimiterName == 'quoteMulti') { // Find the end. Could be either a ' or " delimiter const endRegex = leftText.includes('"') ? /"/g : /'/g; right = _findRightPosition(endRegex, right); replaceLeftOffset += 2; replaceRightOffset -= 1; } else { throw new StitchError('Somehow matched a non-existent delimiter'); } break; } leftDelimiterRegex.lastIndex = right; const code = stripped.slice(left, right); found[type].push({ code, position: left, length: code.length, line: stripped.substring(0, left).split('\n').length, }); stripped = stringReplaceSegment(stripped, left + replaceLeftOffset, right + replaceRightOffset - (left + replaceLeftOffset)); } return { input: gml, stripped, ...found, }; } /** * Find all functions defined in the outer scope for * some chunk of GML. */ export function findOuterFunctions(gml, resource) { let strippedGml = stripCommentsAndStringsFromGml(gml).stripped; const innerScopes = XRegExp.matchRecursive(strippedGml, '{', '}', 'igms').filter((x) => x); for (const innerScope of innerScopes) { // Create a filler with the same string lengh and number of newlines const totalNewlines = countMatches(innerScope, /\n/); const filler = `${' '.repeat(innerScope.length - totalNewlines)}${'\n'.repeat(totalNewlines)}`; strippedGml = strippedGml.replace(innerScope, filler); } const foundFuncs = []; let match; const functionNameRegexGlobal = new RegExp(functionNameRegex, 'g'); while (true) { match = functionNameRegexGlobal.exec(strippedGml); if (!match) { break; } // The location is actually where the `function` token starts, // we want to have the position & column where the *name* starts const offsetPosition = match[0].match(/^(function\s+)/)[1].length; const location = GmlTokenLocation.createFromMatch(strippedGml, match, { offsetPosition, }); location.resource = resource; foundFuncs.push(new GmlToken(match.groups.name, location)); } return foundFuncs; } export function findTokenReferences(gml, token, options) { // The function might already have the suffix. If so, need // to get the basename. const strippedGml = stripCommentsAndStringsFromGml(gml).stripped; const expectedName = typeof token == 'string' ? token : token.name; let basename = expectedName; if (options?.suffixPattern) { const suffixMatch = basename.match(new RegExp(`^(.*?)${options.suffixPattern}$`)); if (suffixMatch) { basename = suffixMatch[1]; } } const functionRegex = new RegExp(`\\b(?<token>${basename}(?<suffix>${options?.suffixPattern || ''}))\\b`, 'g'); const refs = []; let match; while (true) { match = functionRegex.exec(strippedGml); if (!match) { break; } const tokenMatch = match.groups.token; const location = GmlTokenLocation.createFromMatch(strippedGml, match, { sublocation: options?.sublocation, }); location.resource = options?.resource; const ref = new GmlTokenVersioned(tokenMatch, location, expectedName); // Skip if matches search token and excluding self if (typeof token != 'string' && !options?.includeSelf && token.isTheSameToken(ref)) { continue; } refs.push(ref); } return refs; } //# sourceMappingURL=codeParser.js.map