From 114b529f3a50ee8769ee1da55640b1e2a0002d6e Mon Sep 17 00:00:00 2001 From: Nikyle Nguyen Date: Sat, 8 Apr 2023 20:37:57 -0700 Subject: [PATCH] feat: scan .code-formation/*.cfl scripts --- .gitignore | 1 + README.md | 16 +++++ package-lock.json | 13 ++-- package.json | 7 +- src/common.js | 20 +++--- src/eval-snippet-injections.js | 12 ++-- src/index.js | 23 +++++- src/scan-blobs.js | 10 +-- src/scan-scripts.js | 125 +++++++++++++++++++++++++++++++++ src/scan-snippets.js | 9 +-- src/write-output.js | 8 +-- 11 files changed, 206 insertions(+), 38 deletions(-) create mode 100644 src/scan-scripts.js diff --git a/.gitignore b/.gitignore index 6704566..d3ad8c1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +archive # Logs logs *.log diff --git a/README.md b/README.md index 70b11c4..5dcb459 100644 --- a/README.md +++ b/README.md @@ -100,6 +100,22 @@ If you want to uninstall: npm rm -g code-formation ``` + + # 📝 Command Line Interface Usage **code-formation** --scan *[file glob patterns]* --outdir *[base path for output files]* diff --git a/package-lock.json b/package-lock.json index f09a1eb..cb139df 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "code-formation", - "version": "0.3.0", + "version": "0.7.0", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -680,9 +680,9 @@ } }, "s-expression.js": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/s-expression.js/-/s-expression.js-0.4.0.tgz", - "integrity": "sha512-GZ3NKxhWE+lgWyP41tkx4AB3ANlYR1pa72lEJi/AHRL4cbRTy770RzbGD9q6fsPPwdleq+d3mTQYs4fr68Y2Ng==" + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/s-expression.js/-/s-expression.js-0.6.2.tgz", + "integrity": "sha512-rVfvgonS3qlHNeCBPtsXE8p4Ng/8QOYCCsOmRtvTo9YMlLTN7hf97zEFsHS51T2LvmBuCYmqR3WUvpMrnDWoAA==" }, "safe-buffer": { "version": "5.2.1", @@ -809,6 +809,11 @@ "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=" }, + "wildcard-match": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/wildcard-match/-/wildcard-match-5.1.2.tgz", + "integrity": "sha512-qNXwI591Z88c8bWxp+yjV60Ch4F8Riawe3iGxbzquhy8Xs9m+0+SLFBGb/0yCTIDElawtaImC37fYZ+dr32KqQ==" + }, "winston": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/winston/-/winston-3.6.0.tgz", diff --git a/package.json b/package.json index a6e97c1..dfca597 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "code-formation", - "version": "0.6.1", + "version": "0.7.0", "description": "context-free text manipulator using line-oriented DSL for easy embedding to existing source code", "main": "./src/index.js", "bin": "./src/index.js", @@ -11,7 +11,7 @@ ], "scripts": { "test": "echo \"Error: no test specified\" && exit 1" - }, + }, "repository": { "type": "git", "url": "git+https://github.com/NLKNguyen/code-formation.git" @@ -41,8 +41,9 @@ "parse-pairs": "^1.1.0", "query-string": "^7.1.1", "requireg": "^0.2.2", - "s-expression.js": "^0.4.0", + "s-expression.js": "^0.6.2", "vm2": "^3.9.11", + "wildcard-match": "^5.1.2", "winston": "^3.6.0" }, "keywords": [ diff --git a/src/common.js b/src/common.js index 11d4fa5..7802c18 100644 --- a/src/common.js +++ b/src/common.js @@ -12,7 +12,7 @@ const cheerio = require("cheerio") // jQuery for Node const chalk = require("chalk") // Colorful console log const profile = { - marker_prefix: "", + MARKER_PREFIX: "", variables: {}, snippets: {}, exports: [], @@ -36,7 +36,7 @@ const modules = { marked, cheerio, chalk, - "URLSearchParams": URLSearchParams + URLSearchParams: URLSearchParams, } invoke = async (snippet, context) => { @@ -89,7 +89,7 @@ invoke = async (snippet, context) => { requireg, _common: module.exports /* this same module */, _logger: logger, - _: _ + _: _, }, }) const func = vm.run(script) @@ -155,7 +155,7 @@ async function parseParams(str) { // }, // } - return await S.interpret(S.parse(`(${str})`), { + return await S.interpret(S.parse(str, { includedRootParentheses: false }), { handlers: { APPLY: { evaluate: async (components, context, state, entity) => { @@ -284,7 +284,7 @@ function hasTemplateInstruction(profile, line) { // TODO: snippet block // TODO: anchor block const snippetOpenRegex = new RegExp( - `(?` + `(?` ) if (snippetCloseRegex.exec(line)) { return true } const blobOpenRegex = new RegExp( - `(?`) + const blobCloseRegex = new RegExp(`(?`) if (blobCloseRegex.exec(line)) { return true @@ -318,7 +318,7 @@ function hasTemplateInstruction(profile, line) { /** * Check if a path is a local file path, meaning that not have prefix . nor absolute path * @param {string} path to check if it's a local file path - * @returns + * @returns */ function isLocalFilePath(path) { return path && !/(^\.\/)|(^\.\\)|(^\/)|(^[^:\s]+:)/.test(path) @@ -337,5 +337,5 @@ module.exports = { writeFileSyncRecursive, isOnlyOneDefined, hasTemplateInstruction, - isLocalFilePath + isLocalFilePath, } diff --git a/src/eval-snippet-injections.js b/src/eval-snippet-injections.js index fb8e694..e764e0f 100644 --- a/src/eval-snippet-injections.js +++ b/src/eval-snippet-injections.js @@ -32,7 +32,7 @@ async function evalBlock( let line = lines[injection.lineNumber] - let openRegex = openRegexCallback(common.profile.marker_prefix) + let openRegex = openRegexCallback(common.profile.MARKER_PREFIX) let matched = openRegex.exec(line) if (!matched) { return null @@ -48,7 +48,7 @@ async function evalBlock( // console.log(`injection.command = '${injection.command}'`) // console.log(`injection.snippetInjection = ${injection.snippetInjection}`) if (closeRegexCallback) { - const closeRegex = closeRegexCallback(common.profile.marker_prefix, label) + const closeRegex = closeRegexCallback(common.profile.MARKER_PREFIX, label) injection.lineNumber = line_number while (++injection.lineNumber < lines.length) { @@ -95,7 +95,7 @@ async function evalBlock( const expansion = common.serializeMacro(`(${macro})`) lines[line_number] = line.replace(openRegex, () => { - return macroExpansionCallback(common.profile.marker_prefix, label, expansion) + return macroExpansionCallback(common.profile.MARKER_PREFIX, label, expansion) }) logger.info( `${chalk.cyan(`expand macro "@${snippet_name}":`)} ${chalk.gray( @@ -133,15 +133,15 @@ async function evalSnippetInjection(content, params, profile, log) { const result = [] // const blockSnippetOpenRegex = new RegExp( - // `(? for dotenv file ]) - scan = [".code-formation/**", ...scan.split(",").map((e) => e.trim())].filter(Boolean) + scan = [ + ".code-formation/**", + ...scan.split(",").map((e) => e.trim()), + ].filter(Boolean) let sourceFiles = [] for (let p of glob.sync(scan)) { @@ -78,7 +84,7 @@ const logger = require("./logger.js") } } - // if (!_.isUndefined(define)) { + // if (!_.isUndefined(define)) { // try { // define = parsePairs.default(define) // } catch (e) { @@ -93,6 +99,19 @@ const logger = require("./logger.js") })}` ) + const isCflFile = wcmatch("**/.code-formation/*.cfl") + + const cflFiles = sourceFiles.filter(isCflFile) + + if (cflFiles.length > 0) { + logger.info( + `${chalk.gray(`detected CFL scripts:`)} ${colorize(cflFiles, { + pretty: true, + })}` + ) + await scan_scripts(cflFiles, common.profile, logger) + } + // if (!_.isUndefined(define)) { // common.profile.variables = define // } diff --git a/src/scan-blobs.js b/src/scan-blobs.js index 520334a..fe27cf7 100644 --- a/src/scan-blobs.js +++ b/src/scan-blobs.js @@ -38,13 +38,13 @@ module.exports = async function (files, profile, log) { let openTag let doneInterpolation = false // let openRegex = new RegExp( - // `(? `${profile.marker_prefix}!${label}<:${expansion}` + () => `${profile.MARKER_PREFIX}!${label}<:${expansion}` ) logger.info( `${chalk.cyan( @@ -252,7 +252,7 @@ module.exports = async function (files, profile, log) { // const regex = new RegExp(`${focused_entity.tag}>!(.*)`, "g") const regex = new RegExp( - `(?` + `(?` ) const closeTag = line.match(regex) if (closeTag) { diff --git a/src/scan-scripts.js b/src/scan-scripts.js new file mode 100644 index 0000000..3a27f5a --- /dev/null +++ b/src/scan-scripts.js @@ -0,0 +1,125 @@ +const path = require("path") +const fs = require("fs") +const _ = require("lodash") +const chalk = require("chalk") + +const SExpr = require("s-expression.js") +const common = require("./common") + +const logger = require("./logger") +const colorize = require("json-colorizer") + +module.exports = async function (files, profile, log) { + for (let file of files) { + logger.info(`${chalk.magenta(`scan CFL script`)} ${chalk.gray(file)}`) + + const content = fs.readFileSync(file, "utf8").trim() + + const S = new SExpr() + + await S.interpret(S.parse(content, { includedRootParentheses: false }), { + handlers: { + DEF: { + notation: null, + evaluate: async (components, context, state, entity) => { + const throwGenericException = () => { + throw Error( + `Invalid CFL syntax: "(def ${JSON.stringify( + S.serialize(components, { + includingRootParentheses: false, + }) + )})" \nExpected: "(def [NAME] [VALUE])"` + ) + } + + if (components.length !== 2) { + throwGenericException() + } + const name = S.first(components) + if (!S.isAtom(name)) { + throwGenericException() + } + + logger.info( + `${chalk.cyan(`defined variable`)} ${chalk.green(name)}` + ) + + if (_.includes(["variables", "snippets", "exports"], name)) { + throw Error("can't use reserved name") + } + const value = S.valueOf(S.second(components)) + + // logger.info( + // `${chalk.cyan(`define variable`)} ${chalk.green( + // name + // )} ${chalk.cyan(`as`)} ${chalk.green(JSON.stringify(value))} ` + // ) + _.set(profile, name, value) + return true // not needed + }, + }, + + DEFMACRO: { + notation: null, + evaluate: async (components, context, state, entity) => { + const throwGenericException = () => { + throw Error( + `Invalid CFL syntax: "(defmacro ${JSON.stringify( + S.serialize(components, { + includingRootParentheses: false, + }) + )})" \nExpected: "(defmacro ([NAME] [PARAMS]) ([EXPANSION]))"` + ) + } + + if (components.length !== 2) { + throwGenericException() + } + const macro = S.first(components) + const expansion = S.serialize(S.second(components), { + includingRootParentheses: false, + }) + + const name = S.first(macro) + if (!S.isAtom(name)) { + throwGenericException() + } + + const rest = S.serialize(S.rest(macro), { + includingRootParentheses: false, + }) + + let params = await common.parseParams(rest) + + logger.info(`${chalk.cyan(`defined macro`)} ${chalk.green(name)}`) + // console.dir({ + // name, + // params, + // expansion, + // }) + + let LANGUAGE = _.get(params, "LANGUAGE", "ejs") + _.set(params, "LANGUAGE", LANGUAGE) + + const new_snippet = { + kind: "snippet", + tag: "", + params, + src: file, + start: 0, + end: 0, + template: expansion.split("\n"), + custom: {}, + } + + _.set(profile, ["snippets", name], new_snippet) + + return true // not needed + }, + }, + }, + }) + + // console.dir(profile) + } +} diff --git a/src/scan-snippets.js b/src/scan-snippets.js index 2781ec4..bcde8de 100644 --- a/src/scan-snippets.js +++ b/src/scan-snippets.js @@ -24,16 +24,16 @@ module.exports = async function (files, profile, log) { const line = lines[line_index] if (_.isUndefined(focused_entity)) { // const regex = new RegExp( - // `(?` + `(?` ) const closeTag = line.match(regex) if (closeTag) { diff --git a/src/write-output.js b/src/write-output.js index 194c4e7..7cfd5a9 100644 --- a/src/write-output.js +++ b/src/write-output.js @@ -57,8 +57,8 @@ module.exports = async function (files, profile, log) { const content = fs.readFileSync(output_path, "utf8").trim() const lines = content.split(/\r?\n/) const openTagRegex = new RegExp( - // `(?` - `(?` + // `(?` + `(?` ) const closeTag = closeTagRegex.exec(line) if (closeTag) {