UNPKG

php-htx-cli

Version:

A cli to convert .htx (different components) files to .php

353 lines (323 loc) 14.5 kB
import fs from "fs"; import path from "path"; import { htxConfig } from "../config/config"; import REGEX from "./regex"; import { toPhpFormat } from "./parse"; import { Scope, checkForExistingScopes, handleScopes } from "./scopes"; import { isTag } from "./tagDetection"; function usesFromImportSyntax(importParamaters: string[]): boolean { return importParamaters.length >= 3 && importParamaters[1] == "from"; } function parseImportStatement(match: string, config: htxConfig) { let importParamaters = match.slice("<!--".length, -"-->".length).trim().slice("import ".length).split(" "); if (usesFromImportSyntax(importParamaters)) { return { name: importParamaters[0], file: importParamaters.slice(2).join(" ").slice(1, -1) + "." + config.extension.src, fileNameWithoutExtension: importParamaters.slice(2).join(" ").slice(1, -1) }; } let fileNameWithoutExtension = importParamaters.join(" ").slice(1, -1); return { name: path.basename(fileNameWithoutExtension), file: fileNameWithoutExtension + "." + config.extension.src, fileNameWithoutExtension: fileNameWithoutExtension }; } function convert(code: string, uid: number, props: { [index: string]: any }, children = "", config: htxConfig, filePath: string, parentComponentName: string, parentUid: number, isRoot: boolean = false): { output: string, uid: number, possibleScopes: { css: Set<string>, js: Set<string> }, manualScopes: { css: Set<string>, js: Set<string> } } { const localComponentName = path.basename(filePath, "." + config.extension.src); const localUid = uid; let components: { [index: string]: { relativePath: string, contents: string } } = {}; let componentChildStack: [{ component: string | null, attr: { [index: string]: any }, children: string }] = [{ component: null, attr: {}, children: "" }]; const addToOutput = (...content: string[]) => { componentChildStack[componentChildStack.length - 1].children += content.join(""); } let localFunctions: string[] = []; let possibleScopedCssFiles: Set<string> = new Set(); let possibleScopedJsFiles: Set<string> = new Set(); let manualScopedCssFiles: Set<string> = new Set(); let manualScopedJsFiles: Set<string> = new Set(); let isInPhp = false; let isInString: false | `"` | `'` = false; let skipNext = false; for (let charIndex = 0; charIndex < code.length; charIndex++) { let match; // handle php open and close if ((match = code.slice(charIndex).match(REGEX.php.open)) != null && isInString == false) { isInPhp = true; addToOutput(match[0]); charIndex += match[0].length - 1; continue; } if ((match = code.slice(charIndex).match(REGEX.php.close)) != null && isInString == false) { isInPhp = false; addToOutput(match[0]); charIndex += match[0].length - 1; continue; } // hanlde php string if (isInPhp && code[charIndex] == "\"" && !isInString) { isInString = "\""; addToOutput("\""); continue; } if (isInPhp && code[charIndex] == "\"" && isInString == "\"" && !skipNext) { isInString = false; addToOutput("\""); continue; } if (isInPhp && code[charIndex] == "'" && !isInString && !skipNext) { isInString = "'"; addToOutput("'"); continue; } if (isInPhp && code[charIndex] == "'" && isInString == "'" && !skipNext) { isInString = false; addToOutput("'"); continue; } // import statement if ((match = code.slice(charIndex).match(REGEX.importStatement)) != null && !isInPhp) { let { name: componentName, file: componentFileName, fileNameWithoutExtension } = parseImportStatement(match[0], config); const fullPathToFileBase = path.join(path.dirname(path.isAbsolute(filePath) ? filePath : path.join(process.cwd(), filePath)), fileNameWithoutExtension); components[componentName] = { relativePath: path.relative(process.cwd(), fullPathToFileBase), contents: fs.readFileSync(fullPathToFileBase + "." + config.extension.src, 'utf8') }; possibleScopedCssFiles.add(fullPathToFileBase + ".css"); possibleScopedJsFiles.add(fullPathToFileBase + ".js"); charIndex += match[0].length - 1; continue; } // Component children if ((match = code.slice(charIndex).match(REGEX.componentTag.children)) != null && !isInPhp) { let childrenString = ""; if (config.environment == "dev") { childrenString += `\n<!-- DEBUG: Children start ${localComponentName} #${localUid} -->\n`; } childrenString += children; if (config.environment == "dev") { childrenString += `\n<!-- DEBUG: Children end ${localComponentName} #${localUid} -->\n`; } addToOutput(childrenString); charIndex += match[0].length - 1; continue; } // Component scopes if ((match = code.slice(charIndex).match(REGEX.componentTag.scopes)) != null && !isInPhp) { if (config.environment == "dev") addToOutput("\n<!-- DEBUG: Start scopes -->\n"); addToOutput(match[0]); if (config.environment == "dev") addToOutput("\n<!-- DEBUG: End scopes -->\n"); charIndex += match[0].length - 1; continue; } // Component scoped JS if ((match = isTag("/>", code, charIndex)) != false && match.tagname == "JS" && !isInPhp) { manualScopedJsFiles.add(match.attr.src); charIndex += match.size - 1; continue; } // TODO Component scoped CSS if ((match = isTag("/>", code, charIndex)) != false && match.tagname == "CSS" && !isInPhp) { manualScopedCssFiles.add(match.attr.src); charIndex += match.size - 1; continue; } // Component open if ((match = isTag(">", code, charIndex)) != false && !isInPhp) { componentChildStack.push({ component: match.tagname, attr: match.attr, children: "" }); charIndex += match.size - 1; continue; } // Component close if ((match = code.slice(charIndex).match(REGEX.componentTag.close)) != null && !isInPhp) { let currentChildStackContent = componentChildStack.pop(); uid++; if (currentChildStackContent?.component == null) throw new Error(`Cannot access component without a name`); if (!(currentChildStackContent?.component in components)) throw new Error(`Unknown component "${currentChildStackContent?.component}"`); let { output: converted, uid: newUid, possibleScopes, manualScopes } = convert(components[currentChildStackContent?.component].contents, uid, currentChildStackContent?.attr, currentChildStackContent?.children, config, components[currentChildStackContent?.component].relativePath, localComponentName, localUid); possibleScopes.css.forEach(scope => { possibleScopedCssFiles.add(scope); }); possibleScopes.js.forEach(scope => { possibleScopedJsFiles.add(scope); }); manualScopes.css.forEach(scope => { manualScopedCssFiles.add(scope); }); manualScopes.js.forEach(scope => { manualScopedJsFiles.add(scope); }); uid = newUid; addToOutput(converted) charIndex += match[0].length - 1; continue; } // Component single if ((match = isTag("/>", code, charIndex)) != false && !isInPhp) { uid++; if (!(match.tagname in components)) { throw new Error(`Unknown component "${match.tagname}" in file "${filePath}"`); } let { output: converted, uid: newUid, possibleScopes, manualScopes } = convert(components[match.tagname].contents, uid, match.attr, "", config, components[match.tagname].relativePath, localComponentName, localUid); possibleScopes.css.forEach(scope => { possibleScopedCssFiles.add(scope); }); possibleScopes.js.forEach(scope => { possibleScopedJsFiles.add(scope); }); manualScopes.css.forEach(scope => { manualScopedCssFiles.add(scope) }); manualScopes.js.forEach(scope => { manualScopedJsFiles.add(scope) }); uid = newUid; addToOutput(converted); charIndex += match.size - 1; continue; } // Global variable if ((match = code.slice(charIndex).match(REGEX.variable.global)) != null && isInPhp && (isInString == false || skipNext == false)) { addToOutput("$" + match[0].slice("$GLOBAL_".length)); charIndex += match[0].length - 1; continue; } // Local variable if ((match = code.slice(charIndex).match(REGEX.variable.local)) != null && isInPhp && (isInString == false || skipNext == false)) { addToOutput("$" + config.constant.variable .replace("<component>", localComponentName) .replace("<uid>", localUid.toString()) .replace("<variable>", match[0].slice(1)) ); charIndex += match[0].length - 1; continue; } // Create local function if ((match = code.slice(charIndex).match(REGEX.function.define)) != null && isInPhp && isInString == false) { const functionName = match[0].slice("function ".length, -1).trim(); if (functionName.startsWith("GLOBAL_")) { addToOutput(`function ${functionName.slice("GLOBAL_".length)}(`); charIndex += match[0].length - 1; continue; } localFunctions.push(functionName); addToOutput(`function ${config.constant.variable .replace("<component>", localComponentName) .replace("<uid>", localUid.toString()) .replace("<variable>", functionName)}(`); charIndex += match[0].length - 1; continue; } if ((match = code.slice(charIndex).match(REGEX.function.call)) != null && isInPhp && isInString == false) { const functionName = match[0].slice(0, -1).trim(); if ((!functionName.startsWith("GLOBAL_")) && localFunctions.includes(functionName)) { addToOutput(config.constant.variable .replace("<component>", localComponentName) .replace("<uid>", localUid.toString()) .replace("<variable>", functionName) + "("); } else if (functionName.startsWith("GLOBAL_")) { addToOutput(functionName.slice("GLOBAL_".length) + "("); } else { addToOutput(functionName + "("); } charIndex += match[0].length - 1; continue; } if (isInPhp && isInString != false && code[charIndex] == "\\") { skipNext = true; addToOutput("\\"); continue; } else { skipNext = false; } addToOutput(code[charIndex]); } // auto close php at end of file if (isInString && isInPhp) { addToOutput(`";?>`); } else if (isInPhp) { addToOutput("?>"); } if (isRoot) { possibleScopedCssFiles.add(filePath.slice(0, -(("." + config.extension.src).length)) + ".css"); possibleScopedJsFiles.add(filePath.slice(0, -(("." + config.extension.src).length)) + ".js"); } const manualFileToScope = function (manualFile: string, scopeType: "css" | "js"): Scope { return { type: scopeType, remote: true, path: manualFile }; } let existingScopes: Scope[] | undefined; if (isRoot) { existingScopes = checkForExistingScopes(possibleScopedCssFiles, possibleScopedJsFiles); existingScopes?.push( ...Array.from(manualScopedCssFiles) .map(manualFile => manualFileToScope(manualFile, "css")) ); existingScopes?.push( ...Array.from(manualScopedJsFiles) .map(manualFile => manualFileToScope(manualFile, "js")) ); } let finalOutput = (config.environment == "dev") ? `\n<!-- DEBUG: Start ${localComponentName} #${localUid} -->\n` : ""; // add props variable finalOutput += `<?php ${"$" + config.constant.variable .replace("<component>", localComponentName) .replace("<uid>", localUid.toString()) .replace("<variable>", config.constant.props)} = ${toPhpFormat(props, parentComponentName, parentUid, config)}; ?>`; // handle scopes (only in root, because it has to be called once and contain all local scripts and styles) finalOutput += isRoot ? handleScopes(componentChildStack[0].children, existingScopes as Scope[], config.environment == "dev") : componentChildStack[0].children; if (config.environment == "dev") { finalOutput += `\n<!-- DEBUG: End ${localComponentName} #${localUid} -->\n`; } return { output: finalOutput, uid, possibleScopes: { css: possibleScopedCssFiles, js: possibleScopedJsFiles }, manualScopes: { css: manualScopedCssFiles, js: manualScopedJsFiles } }; } function removeEmptyLines(line: string) { return line.replace(/\r/g, "").trim() != ""; } export function convertHtx(contents: string, filePath: string, config: htxConfig): string { const converted = convert(contents, 0, {}, "", config, filePath, "", -1, true).output; return converted.split("\n").filter(removeEmptyLines).join("\n"); }