diff --git a/.github/workflows/nx-report.yml b/.github/workflows/nx-report.yml index 188f5af..b089c8c 100644 --- a/.github/workflows/nx-report.yml +++ b/.github/workflows/nx-report.yml @@ -14,11 +14,11 @@ jobs: init-commands: | npx nx-cloud start-ci-run --agent-count=3 parallel-commands: | - npx nx-cloud record -- npx nx affected --target=build + npx nx-cloud record -- npx nx affected --target=build:all npx nx-cloud record -- npx nx affected --target=typecheck npx nx-cloud record -- npx nx affected --target=test parallel-commands-on-agents: | - npx nx affected --target=build --parallel=3 + npx nx affected --target=build:all --parallel=3 npx nx affected --target=typecheck --parallel=3 npx nx affected --target=test --parallel=3 diff --git a/.vscode/settings.json b/.vscode/settings.json index a8122d6..0387840 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -3,5 +3,8 @@ "Parser.ts": "i18n", "Tokenizer.ts": "i18n" }, - "material-icon-theme.folders.associations": {} + "material-icon-theme.folders.associations": {}, + "editor.insertSpaces": false, + "editor.detectIndentation": false, + "editor.trimAutoWhitespace": false } diff --git a/CHANGELOG.md b/CHANGELOG.md index 84b75d7..77d88e5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,21 +1,42 @@ -## [1.0.0-rc.2](https://github.com/formkl/formkl/compare/1.0.0-rc.1...1.0.0-rc.2) (2022-11-23) +## [1.0.0-rc.3](https://github.com/imrim12/formkl/compare/1.0.0-rc.2...1.0.0-rc.3) (2022-12-25) ### Features -* added husky git hooks ([966c15f](https://github.com/formkl/formkl/commit/966c15f7dae5aeebb7525104a135c4c9e7a4faad)) -* added release-it package for release ([e46d2c0](https://github.com/formkl/formkl/commit/e46d2c06161baea229213e5bb26be6386f44bd10)) -* support form lifecycle hooks ([d2aff34](https://github.com/formkl/formkl/commit/d2aff3436ad0c9e77b4d3728f0dcfe915f6be804)) +* add stringifier ([8c8d13e](https://github.com/imrim12/formkl/commit/8c8d13e22c4f6cfd2707ec0423700590bc7bb62a)) +* added husky git hooks ([966c15f](https://github.com/imrim12/formkl/commit/966c15f7dae5aeebb7525104a135c4c9e7a4faad)) +* added release-it package for release ([e46d2c0](https://github.com/imrim12/formkl/commit/e46d2c06161baea229213e5bb26be6386f44bd10)) +* handle keyword value in validation ([45972c6](https://github.com/imrim12/formkl/commit/45972c6a4116fe34881898adb1d2554f15bd9e6c)) +* support form lifecycle hooks ([d2aff34](https://github.com/imrim12/formkl/commit/d2aff3436ad0c9e77b4d3728f0dcfe915f6be804)) ### Bug Fixes -* always return true when use regex with logic validator ([3dd1e41](https://github.com/formkl/formkl/commit/3dd1e418f181390600c656f6956b28d1bc8f99ec)) -* build fail ([51298d8](https://github.com/formkl/formkl/commit/51298d80524a2359195d3f861208c1b06e0108df)) -* circular dependencies build fail nx ([d6c59a6](https://github.com/formkl/formkl/commit/d6c59a68df2c44b9f1d42018bb95ffef7b435605)) -* editor web component lifecycle works differently on vue/react ([7d006b7](https://github.com/formkl/formkl/commit/7d006b776c28ef33682cd0906c241ae4e78cb5e7)) -* setup vitest component testing ([8a3f07d](https://github.com/formkl/formkl/commit/8a3f07df969299dadb78e4c3b8e96b3ccbaf3dac)) -* tsconfig.json ([d7657fa](https://github.com/formkl/formkl/commit/d7657fa536358d70136f328b12c8b5d52b600f77)) -* vercel build fail ([8ad35a6](https://github.com/formkl/formkl/commit/8ad35a6a93a25e6d1ae65b2731db73360e870a70)) \ No newline at end of file +* always return true when use regex with logic validator ([3dd1e41](https://github.com/imrim12/formkl/commit/3dd1e418f181390600c656f6956b28d1bc8f99ec)) +* circular dependencies build fail nx ([d6c59a6](https://github.com/imrim12/formkl/commit/d6c59a68df2c44b9f1d42018bb95ffef7b435605)) +* editor web component lifecycle works differently on vue/react ([7d006b7](https://github.com/imrim12/formkl/commit/7d006b776c28ef33682cd0906c241ae4e78cb5e7)) +* setup vitest component testing ([8a3f07d](https://github.com/imrim12/formkl/commit/8a3f07df969299dadb78e4c3b8e96b3ccbaf3dac)) +* tsconfig.json ([d7657fa](https://github.com/imrim12/formkl/commit/d7657fa536358d70136f328b12c8b5d52b600f77)) +* vercel build fail ([8ad35a6](https://github.com/imrim12/formkl/commit/8ad35a6a93a25e6d1ae65b2731db73360e870a70)) + +## [1.0.0-rc.2](https://github.com/imrim12/formkl/compare/1.0.0-rc.1...1.0.0-rc.2) (2022-11-23) + + +### Features + +* added husky git hooks ([966c15f](https://github.com/imrim12/formkl/commit/966c15f7dae5aeebb7525104a135c4c9e7a4faad)) +* added release-it package for release ([e46d2c0](https://github.com/imrim12/formkl/commit/e46d2c06161baea229213e5bb26be6386f44bd10)) +* support form lifecycle hooks ([d2aff34](https://github.com/imrim12/formkl/commit/d2aff3436ad0c9e77b4d3728f0dcfe915f6be804)) + + +### Bug Fixes + +* always return true when use regex with logic validator ([3dd1e41](https://github.com/imrim12/formkl/commit/3dd1e418f181390600c656f6956b28d1bc8f99ec)) +* build fail ([51298d8](https://github.com/imrim12/formkl/commit/51298d80524a2359195d3f861208c1b06e0108df)) +* circular dependencies build fail nx ([d6c59a6](https://github.com/imrim12/formkl/commit/d6c59a68df2c44b9f1d42018bb95ffef7b435605)) +* editor web component lifecycle works differently on vue/react ([7d006b7](https://github.com/imrim12/formkl/commit/7d006b776c28ef33682cd0906c241ae4e78cb5e7)) +* setup vitest component testing ([8a3f07d](https://github.com/imrim12/formkl/commit/8a3f07df969299dadb78e4c3b8e96b3ccbaf3dac)) +* tsconfig.json ([d7657fa](https://github.com/imrim12/formkl/commit/d7657fa536358d70136f328b12c8b5d52b600f77)) +* vercel build fail ([8ad35a6](https://github.com/imrim12/formkl/commit/8ad35a6a93a25e6d1ae65b2731db73360e870a70)) \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6899358..16b06fd 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -8,8 +8,8 @@ Please make sure to read the [Contributing Guide](https://formkl.org/learning/co Thank you to all the people who already contributed to Vue! - - + + Made with [contrib.rocks](https://contrib.rocks). diff --git a/README.md b/README.md index 30abe77..7104edc 100644 --- a/README.md +++ b/README.md @@ -14,8 +14,8 @@ Please make sure to read the [Contributing Guide](https://formkl.org/learning/co Thank you to all the people who already contributed to the Formkl - Form Markup Language project! - - + + Made with [contrib.rocks](https://contrib.rocks). diff --git a/docs/.vitepress/config.js b/docs/.vitepress/config.js index fe8be8e..1829d96 100644 --- a/docs/.vitepress/config.js +++ b/docs/.vitepress/config.js @@ -9,10 +9,10 @@ export default { nav: [{ text: "Syntax guide", link: "/syntax/form" }], - socialLinks: [{ icon: "github", link: "https://github.com/formkl/formkl" }], + socialLinks: [{ icon: "github", link: "https://github.com/imrim12/formkl" }], editLink: { - pattern: "https://github.com/formkl/formkl/edit/main/docs/:path", + pattern: "https://github.com/imrim12/formkl/edit/main/docs/:path", text: "Edit this page on GitHub", }, diff --git a/docs/index.md b/docs/index.md index ef87dba..d2204d9 100644 --- a/docs/index.md +++ b/docs/index.md @@ -17,7 +17,7 @@ hero: link: /introduction - theme: alt text: Star on GitHub - link: https://github.com/formkl/formkl + link: https://github.com/imrim12/formkl features: - icon: ⚡️ diff --git a/docs/learning/contribution-guide.md b/docs/learning/contribution-guide.md index 93ef8f8..e19cf05 100644 --- a/docs/learning/contribution-guide.md +++ b/docs/learning/contribution-guide.md @@ -27,9 +27,9 @@ More about pnpm, please refer to [pnpm.io](https://pnpm.io), if you don't have i npm i -g pnpm ``` -Then clone the [repository](https://github.com/formkl/formkl) and open the cloned directory with your code editor or IDE. +Then clone the [repository](https://github.com/imrim12/formkl) and open the cloned directory with your code editor or IDE. ```bash -git clone git@github.com:formkl/formkl.git +git clone git@github.com:imrim12/formkl.git ``` Install the workspace dependencies diff --git a/docs/learning/editor.md b/docs/learning/editor.md index 9a67084..91c0efc 100644 --- a/docs/learning/editor.md +++ b/docs/learning/editor.md @@ -17,7 +17,7 @@ But in our case, this is a brand new language and we need to build the syntax hi We use [`@codemirror/autocomplete`](https://codemirror.net/6/docs/ref/#autocomplete) to define a set of keyword and definitions for auto complete. -The code is located at [`packages/editor/src/extensions/autocomplete.ts`](https://github.com/formkl/formkl/blob/9b5537cd326534208e2154b50664d9d098fb7113/packages/editor/src/extensions/autocomplete.ts) +The code is located at [`packages/editor/src/extensions/autocomplete.ts`](https://github.com/imrim12/formkl/blob/9b5537cd326534208e2154b50664d9d098fb7113/packages/editor/src/extensions/autocomplete.ts) ## Syntax Error Checker @@ -25,4 +25,4 @@ Error Checker or as known as Linter is a very important part of the editor. It h We use our own [FORMKL Parser](/introduction#basic-example) to parse the syntax, it would be the correct syntax if it's parsed successfully. Otherwise, the parser will throw a syntax error. -The code is located at [`packages/editor/src/extensions/lint.ts`](https://github.com/formkl/formkl/blob/9b5537cd326534208e2154b50664d9d098fb7113/packages/editor/src/extensions/lint.ts). +The code is located at [`packages/editor/src/extensions/lint.ts`](https://github.com/imrim12/formkl/blob/9b5537cd326534208e2154b50664d9d098fb7113/packages/editor/src/extensions/lint.ts). diff --git a/package.json b/package.json index d15eab9..23cb105 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,9 @@ "description": "Form markup language", "packageManager": "pnpm@7.12.2", "scripts": { - "build": "nx run-many --target=build --all", + "nx": "nx", + "build": "nx build", + "build:all": "nx run-many --target=build --all", "typecheck": "nx run-many --target=typecheck --all", "test": "vitest run", "test:ui": "vitest --ui", @@ -17,7 +19,7 @@ }, "repository": { "type": "git", - "url": "git+https://github.com/formkl/formkl.git" + "url": "git+https://github.com/imrim12/formkl.git" }, "keywords": [ "form", @@ -30,9 +32,9 @@ "author": "thecodeorigin", "license": "MIT", "bugs": { - "url": "https://github.com/formkl/formkl/issues" + "url": "https://github.com/imrim12/formkl/issues" }, - "homepage": "https://github.com/formkl/formkl#readme", + "homepage": "https://github.com/imrim12/formkl#readme", "workspaces": [ "packages/**", "sandbox", @@ -64,10 +66,11 @@ "nx": "^15.0.0", "release-it": "^15.5.0", "rimraf": "^3.0.2", + "ts-node": "^10.9.1", "typescript": "^4.8.4", "vitest": "^0.24.4", "vue": "^3.2.41", "vue-tsc": "^1.0.9" }, - "version": "1.0.0-rc.2" + "version": "1.0.0-rc.3" } diff --git a/packages/adapters/vue/package.json b/packages/adapters/vue/package.json index c9ae61d..b8bd82f 100644 --- a/packages/adapters/vue/package.json +++ b/packages/adapters/vue/package.json @@ -1,6 +1,6 @@ { "name": "@formkl/vue", - "version": "1.0.0-rc.2", + "version": "1.0.0-rc.3", "description": "A Vue adapter to generate usable Vue from component from Formkl syntax/schema", "type": "module", "main": "dist/index.es.js", @@ -12,7 +12,7 @@ }, "repository": { "type": "git", - "url": "git+https://github.com/formkl/formkl.git", + "url": "git+https://github.com/imrim12/formkl.git", "directory": "packages/adapters/vue" }, "keywords": [ @@ -21,9 +21,9 @@ "author": "thecodeorigin", "license": "MIT", "bugs": { - "url": "https://github.com/formkl/formkl/issues" + "url": "https://github.com/imrim12/formkl/issues" }, - "homepage": "https://github.com/formkl/formkl#readme", + "homepage": "https://github.com/imrim12/formkl#readme", "dependencies": { "@formkl/plugin-vue": "workspace:*", "@formkl/shared": "workspace:*", diff --git a/packages/editor/package.json b/packages/editor/package.json index a81753d..37c5f71 100644 --- a/packages/editor/package.json +++ b/packages/editor/package.json @@ -1,6 +1,6 @@ { "name": "@formkl/editor", - "version": "1.0.0-rc.2", + "version": "1.0.0-rc.3", "description": "A universal Editor for Formkl using web component API", "type": "module", "exports": { @@ -20,7 +20,7 @@ }, "repository": { "type": "git", - "url": "git+ssh://git@github.com/formkl/formkl.git", + "url": "git+ssh://git@github.com/imrim12/formkl.git", "directory": "packages/editor" }, "keywords": [ @@ -30,9 +30,9 @@ "author": "thecodeorigin", "license": "MIT", "bugs": { - "url": "https://github.com/formkl/formkl/issues" + "url": "https://github.com/imrim12/formkl/issues" }, - "homepage": "https://github.com/formkl/formkl#readme", + "homepage": "https://github.com/imrim12/formkl#readme", "dependencies": { "@codemirror/autocomplete": "^6.2.0", "@codemirror/commands": "^6.1.0", diff --git a/packages/language/README.md b/packages/language/README.md index 30abe77..7104edc 100644 --- a/packages/language/README.md +++ b/packages/language/README.md @@ -14,8 +14,8 @@ Please make sure to read the [Contributing Guide](https://formkl.org/learning/co Thank you to all the people who already contributed to the Formkl - Form Markup Language project! - - + + Made with [contrib.rocks](https://contrib.rocks). diff --git a/packages/language/package.json b/packages/language/package.json index 0bf539b..acc27e6 100644 --- a/packages/language/package.json +++ b/packages/language/package.json @@ -1,6 +1,6 @@ { "name": "formkl", - "version": "1.0.0-rc.2", + "version": "1.0.0-rc.3", "description": "Form markup language", "type": "module", "exports": { @@ -20,7 +20,7 @@ }, "repository": { "type": "git", - "url": "git+https://github.com/formkl/formkl.git", + "url": "git+https://github.com/imrim12/formkl.git", "directory": "language" }, "keywords": [ @@ -34,9 +34,9 @@ "author": "thecodeorigin", "license": "MIT", "bugs": { - "url": "https://github.com/formkl/formkl/issues" + "url": "https://github.com/imrim12/formkl/issues" }, - "homepage": "https://github.com/formkl/formkl#readme", + "homepage": "https://github.com/imrim12/formkl#readme", "dependencies": { "@formkl/shared": "workspace:*", "slugify": "^1.6.5" diff --git a/packages/language/src/__tests__/capitalized-syntax.test.ts b/packages/language/src/__tests__/capitalized-syntax.test.ts index 46f1e5b..5d3d3ca 100644 --- a/packages/language/src/__tests__/capitalized-syntax.test.ts +++ b/packages/language/src/__tests__/capitalized-syntax.test.ts @@ -1,4 +1,4 @@ -import parser from "formkl"; +import parser, { defineForm } from "../"; describe("Used with Capitalized syntax", () => { it("should parse the form syntax correctly", () => { @@ -9,24 +9,26 @@ describe("Used with Capitalized syntax", () => { } }`); - expect(result).toStrictEqual({ - model: "base", - sections: [ - { - fields: [ - { - type: "text", - label: "Text", - key: "text", - }, - { - type: "text", - label: "Another", - key: "another", - }, - ], - }, - ], - }); + expect(result).toStrictEqual( + defineForm({ + model: "base", + sections: [ + { + fields: [ + { + type: "text", + label: "Text", + key: "text", + }, + { + type: "text", + label: "Another", + key: "another", + }, + ], + }, + ], + }), + ); }); }); diff --git a/packages/language/src/__tests__/field-multiple-responses.test.ts b/packages/language/src/__tests__/field-multiple-responses.test.ts index e469a34..33595de 100644 --- a/packages/language/src/__tests__/field-multiple-responses.test.ts +++ b/packages/language/src/__tests__/field-multiple-responses.test.ts @@ -1,4 +1,4 @@ -import parser from "formkl"; +import parser, { defineForm } from "../"; describe("Field with multiple responses support", () => { it("should parse the form syntax correctly", () => { @@ -8,21 +8,23 @@ describe("Field with multiple responses support", () => { } }`); - expect(result).toStrictEqual({ - model: "base", - sections: [ - { - fields: [ - { - type: "text", - label: "Text", - key: "text", - multiple: true, - }, - ], - }, - ], - }); + expect(result).toStrictEqual( + defineForm({ + model: "base", + sections: [ + { + fields: [ + { + type: "text", + label: "Text", + key: "text", + multiple: true, + }, + ], + }, + ], + }), + ); }); it("should parse the form syntax correctly with multiple required fields", () => { @@ -32,22 +34,24 @@ describe("Field with multiple responses support", () => { } }`); - expect(result).toStrictEqual({ - model: "base", - sections: [ - { - fields: [ - { - type: "text", - label: "Text", - key: "text", - required: true, - multiple: true, - }, - ], - }, - ], - }); + expect(result).toStrictEqual( + defineForm({ + model: "base", + sections: [ + { + fields: [ + { + type: "text", + label: "Text", + key: "text", + required: true, + multiple: true, + }, + ], + }, + ], + }), + ); }); it("should parse the form syntax correctly with multiple required fields", () => { @@ -57,21 +61,52 @@ describe("Field with multiple responses support", () => { } }`); - expect(result).toStrictEqual({ - model: "base", - sections: [ - { - fields: [ - { - type: "text", - label: "Text", - key: "text", - required: true, - multiple: true, - }, - ], - }, - ], - }); + expect(result).toStrictEqual( + defineForm({ + model: "base", + sections: [ + { + fields: [ + { + type: "text", + label: "Text", + key: "text", + required: true, + multiple: true, + }, + ], + }, + ], + }), + ); + }); + + it("should stringify the formkl object correctly", () => { + const result = parser.stringify( + defineForm({ + model: "base", + sections: [ + { + fields: [ + { + type: "text", + label: "Text", + key: "text", + required: true, + multiple: true, + }, + ], + }, + ], + }), + ); + + expect(result).toBe( + `formkl { + includes { + require multiple text; + } +}`, + ); }); }); diff --git a/packages/language/src/__tests__/field-required.test.ts b/packages/language/src/__tests__/field-required.test.ts index aac30a8..1310f0b 100644 --- a/packages/language/src/__tests__/field-required.test.ts +++ b/packages/language/src/__tests__/field-required.test.ts @@ -1,4 +1,4 @@ -import parser from "formkl"; +import parser, { defineForm } from "../"; describe("Required field", () => { it("should parse the form syntax correctly", () => { @@ -9,25 +9,59 @@ describe("Required field", () => { } }`); - expect(result).toStrictEqual({ - model: "base", - sections: [ - { - fields: [ - { - type: "text", - label: "Text", - key: "text", - required: true, - }, - { - type: "text", - label: "Not required", - key: "not-required", - }, - ], - }, - ], - }); + expect(result).toStrictEqual( + defineForm({ + model: "base", + sections: [ + { + fields: [ + { + type: "text", + label: "Text", + key: "text", + required: true, + }, + { + type: "text", + label: "Not required", + key: "not-required", + }, + ], + }, + ], + }), + ); + }); + + it("should stringify the form syntax correctly", () => { + const result = parser.stringify( + defineForm({ + model: "base", + sections: [ + { + fields: [ + { + type: "text", + label: "Text", + key: "text", + required: true, + }, + { + type: "text", + label: "Not required", + key: "not-required", + }, + ], + }, + ], + }), + ); + + expect(result).toBe(`formkl { + includes { + require text; + "Not required" text; + } +}`); }); }); diff --git a/packages/language/src/__tests__/field-validation-with-keyword-value.test.ts b/packages/language/src/__tests__/field-validation-with-keyword-value.test.ts new file mode 100644 index 0000000..92571da --- /dev/null +++ b/packages/language/src/__tests__/field-validation-with-keyword-value.test.ts @@ -0,0 +1,197 @@ +import parser, { defineForm } from "../"; + +describe("Field validation using keyword value like null, undefined, NaN", () => { + it("should parse the form syntax correctly", () => { + const result = parser.parse(`formkl { + includes { + text valid(== null); + "Test with OR" text valid(> 5 or == NaN or has "Keyword"); + "Test with AND" text valid(> 5 and == undefined and has "Keyword"); + "Test with Both" text valid(> 5 or == null and has "Keyword"); + } + }`); + + expect(result).toStrictEqual( + defineForm({ + model: "base", + sections: [ + { + fields: [ + { + type: "text", + label: "Text", + key: "text", + validation: { + logic: { + $eq: null, + }, + }, + }, + { + type: "text", + label: "Test with OR", + key: "test-with-or", + validation: { + logic: { + $or: [ + { + $gt: 5, + }, + { + $eq: NaN, + }, + { + $has: "Keyword", + }, + ], + }, + }, + }, + { + type: "text", + label: "Test with AND", + key: "test-with-and", + validation: { + logic: { + $and: [ + { + $gt: 5, + }, + { + $eq: undefined, + }, + { + $has: "Keyword", + }, + ], + }, + }, + }, + { + type: "text", + label: "Test with Both", + key: "test-with-both", + validation: { + logic: { + $or: [ + { + $gt: 5, + }, + { + $and: [ + { + $eq: null, + }, + { + $has: "Keyword", + }, + ], + }, + ], + }, + }, + }, + ], + }, + ], + }), + ); + }); + + it("should stringify the formkl object correctly", () => { + const result = parser.stringify( + defineForm({ + model: "base", + sections: [ + { + fields: [ + { + type: "text", + label: "Text", + key: "text", + validation: { + logic: { + $eq: null, + }, + }, + }, + { + type: "text", + label: "Test with OR", + key: "test-with-or", + validation: { + logic: { + $or: [ + { + $gt: 5, + }, + { + $eq: NaN, + }, + { + $has: "Keyword", + }, + ], + }, + }, + }, + { + type: "text", + label: "Test with AND", + key: "test-with-and", + validation: { + logic: { + $and: [ + { + $gt: 5, + }, + { + $eq: undefined, + }, + { + $has: "Keyword", + }, + ], + }, + }, + }, + { + type: "text", + label: "Test with Both", + key: "test-with-both", + validation: { + logic: { + $or: [ + { + $gt: 5, + }, + { + $and: [ + { + $eq: null, + }, + { + $has: "Keyword", + }, + ], + }, + ], + }, + }, + }, + ], + }, + ], + }), + ); + + expect(result).toBe(`formkl { + includes { + text valid(== null); + "Test with OR" text valid(> 5 or == NaN or has "Keyword"); + "Test with AND" text valid(> 5 and == undefined and has "Keyword"); + "Test with Both" text valid(> 5 or == null and has "Keyword"); + } +}`); + }); +}); diff --git a/packages/language/src/__tests__/field-with-alias.test.ts b/packages/language/src/__tests__/field-with-alias.test.ts index 064c339..93539de 100644 --- a/packages/language/src/__tests__/field-with-alias.test.ts +++ b/packages/language/src/__tests__/field-with-alias.test.ts @@ -1,4 +1,4 @@ -import parser from "formkl"; +import parser, { defineForm } from "../"; describe("Field with alias (Custom key)", () => { it("should parse the form syntax correctly", () => { @@ -8,19 +8,46 @@ describe("Field with alias (Custom key)", () => { } }`); - expect(result).toStrictEqual({ - model: "base", - sections: [ - { - fields: [ - { - type: "text", - label: "Text", - key: "custom-key", - }, - ], - }, - ], - }); + expect(result).toStrictEqual( + defineForm({ + model: "base", + sections: [ + { + fields: [ + { + type: "text", + label: "Text", + key: "custom-key", + }, + ], + }, + ], + }), + ); + }); + + it("should stringify the form syntax correctly", () => { + const result = parser.stringify( + defineForm({ + model: "base", + sections: [ + { + fields: [ + { + type: "text", + label: "Text", + key: "custom-key", + }, + ], + }, + ], + }), + ); + + expect(result).toBe(`formkl { + includes { + text as "custom-key"; + } +}`); }); }); diff --git a/packages/language/src/__tests__/field-with-label.test.ts b/packages/language/src/__tests__/field-with-label.test.ts index 2c319b4..81c75b4 100644 --- a/packages/language/src/__tests__/field-with-label.test.ts +++ b/packages/language/src/__tests__/field-with-label.test.ts @@ -1,4 +1,4 @@ -import parser from "formkl"; +import parser, { defineForm } from "../"; describe("Field with label", () => { it("should parse the form syntax correctly", () => { @@ -8,19 +8,46 @@ describe("Field with label", () => { } }`); - expect(result).toStrictEqual({ - model: "base", - sections: [ - { - fields: [ - { - type: "text", - label: "Some field", - key: "some-field", - }, - ], - }, - ], - }); + expect(result).toStrictEqual( + defineForm({ + model: "base", + sections: [ + { + fields: [ + { + type: "text", + label: "Some field", + key: "some-field", + }, + ], + }, + ], + }), + ); + }); + + it("should stringify the form syntax correctly", () => { + const result = parser.stringify( + defineForm({ + model: "base", + sections: [ + { + fields: [ + { + type: "text", + label: "Some field", + key: "some-field", + }, + ], + }, + ], + }), + ); + + expect(result).toBe(`formkl { + includes { + "Some field" text; + } +}`); }); }); diff --git a/packages/language/src/__tests__/field-with-logic-validation.test.ts b/packages/language/src/__tests__/field-with-logic-validation.test.ts index 5c15e95..a9025b7 100644 --- a/packages/language/src/__tests__/field-with-logic-validation.test.ts +++ b/packages/language/src/__tests__/field-with-logic-validation.test.ts @@ -1,4 +1,4 @@ -import parser from "formkl"; +import parser, { defineForm } from "../"; describe("Field with use of validation", () => { it("should parse the form syntax correctly", () => { @@ -11,88 +11,187 @@ describe("Field with use of validation", () => { } }`); - expect(result).toStrictEqual({ - model: "base", - sections: [ - { - fields: [ - { - type: "text", - label: "Text", - key: "text", - validation: { - logic: { - $gt: 5, + expect(result).toStrictEqual( + defineForm({ + model: "base", + sections: [ + { + fields: [ + { + type: "text", + label: "Text", + key: "text", + validation: { + logic: { + $gt: 5, + }, }, }, - }, - { - type: "text", - label: "Test with OR", - key: "test-with-or", - validation: { - logic: { - $or: [ - { - $gt: 5, - }, - { - $eq: "Some value", - }, - { - $has: "Keyword", - }, - ], + { + type: "text", + label: "Test with OR", + key: "test-with-or", + validation: { + logic: { + $or: [ + { + $gt: 5, + }, + { + $eq: "Some value", + }, + { + $has: "Keyword", + }, + ], + }, }, }, - }, - { - type: "text", - label: "Test with AND", - key: "test-with-and", - validation: { - logic: { - $and: [ - { - $gt: 5, - }, - { - $eq: "Some value", - }, - { - $has: "Keyword", - }, - ], + { + type: "text", + label: "Test with AND", + key: "test-with-and", + validation: { + logic: { + $and: [ + { + $gt: 5, + }, + { + $eq: "Some value", + }, + { + $has: "Keyword", + }, + ], + }, }, }, - }, - { - type: "text", - label: "Test with Both", - key: "test-with-both", - validation: { - logic: { - $or: [ - { - $gt: 5, - }, - { - $and: [ - { - $eq: "Some value", - }, - { - $has: "Keyword", - }, - ], - }, - ], + { + type: "text", + label: "Test with Both", + key: "test-with-both", + validation: { + logic: { + $or: [ + { + $gt: 5, + }, + { + $and: [ + { + $eq: "Some value", + }, + { + $has: "Keyword", + }, + ], + }, + ], + }, }, }, - }, - ], - }, - ], - }); + ], + }, + ], + }), + ); + }); + + it("should stringify the form syntax correctly", () => { + const result = parser.stringify( + defineForm({ + model: "base", + sections: [ + { + fields: [ + { + type: "text", + label: "Text", + key: "text", + validation: { + logic: { + $gt: 5, + }, + }, + }, + { + type: "text", + label: "Test with OR", + key: "test-with-or", + validation: { + logic: { + $or: [ + { + $gt: 5, + }, + { + $eq: "Some value", + }, + { + $has: "Keyword", + }, + ], + }, + }, + }, + { + type: "text", + label: "Test with AND", + key: "test-with-and", + validation: { + logic: { + $and: [ + { + $gt: 5, + }, + { + $eq: "Some value", + }, + { + $has: "Keyword", + }, + ], + }, + }, + }, + { + type: "text", + label: "Test with Both", + key: "test-with-both", + validation: { + logic: { + $or: [ + { + $gt: 5, + }, + { + $and: [ + { + $eq: "Some value", + }, + { + $has: "Keyword", + }, + ], + }, + ], + }, + }, + }, + ], + }, + ], + }), + ); + + expect(result).toBe(`formkl { + includes { + text valid(> 5); + "Test with OR" text valid(> 5 or == "Some value" or has "Keyword"); + "Test with AND" text valid(> 5 and == "Some value" and has "Keyword"); + "Test with Both" text valid(> 5 or == "Some value" and has "Keyword"); + } +}`); }); }); diff --git a/packages/language/src/__tests__/field-with-regex-validation.test.ts b/packages/language/src/__tests__/field-with-regex-validation.test.ts index 31d1a3d..37d57fd 100644 --- a/packages/language/src/__tests__/field-with-regex-validation.test.ts +++ b/packages/language/src/__tests__/field-with-regex-validation.test.ts @@ -1,4 +1,4 @@ -import parser from "formkl"; +import parser, { defineForm } from "../"; describe("Field with use of validation", () => { it("should parse the form syntax correctly", () => { @@ -8,22 +8,52 @@ describe("Field with use of validation", () => { } }`); - expect(result).toStrictEqual({ - model: "base", - sections: [ - { - fields: [ - { - type: "text", - label: "Test with regex", - key: "test-with-regex", - validation: { - regex: /^[0-9]+$/, + expect(result).toStrictEqual( + defineForm({ + model: "base", + sections: [ + { + fields: [ + { + type: "text", + label: "Test with regex", + key: "test-with-regex", + validation: { + regex: /^[0-9]+$/, + }, }, - }, - ], - }, - ], - }); + ], + }, + ], + }), + ); + }); + + it("should stringify the form syntax correctly", () => { + const result = parser.stringify( + defineForm({ + model: "base", + sections: [ + { + fields: [ + { + type: "text", + label: "Test with regex", + key: "test-with-regex", + validation: { + regex: /^[0-9]+$/, + }, + }, + ], + }, + ], + }), + ); + + expect(result).toBe(`formkl { + includes { + "Test with regex" text regex("^[0-9]+$"); + } +}`); }); }); diff --git a/packages/language/src/__tests__/form-with-description.test.ts b/packages/language/src/__tests__/form-with-description.test.ts index e3d1763..65a60f8 100644 --- a/packages/language/src/__tests__/form-with-description.test.ts +++ b/packages/language/src/__tests__/form-with-description.test.ts @@ -1,4 +1,4 @@ -import parser from "formkl"; +import parser, { defineForm } from "../"; describe("Form with description", () => { it("should parse the form syntax correctly", () => { @@ -12,21 +12,50 @@ describe("Form with description", () => { } }`); - expect(result).toStrictEqual({ - model: "base", - title: "Form title (Must has)", - description: "Form description", - sections: [ - { - fields: [ - { - type: "text", - label: "Text", - key: "text", - }, - ], - }, - ], - }); + expect(result).toStrictEqual( + defineForm({ + model: "base", + title: "Form title (Must has)", + description: "Form description", + sections: [ + { + fields: [ + { + type: "text", + label: "Text", + key: "text", + }, + ], + }, + ], + }), + ); + }); + + it("should stringify the form syntax correctly", () => { + const result = parser.stringify( + defineForm({ + model: "base", + title: "Form title (Must has)", + description: "Form description", + sections: [ + { + fields: [ + { + type: "text", + label: "Text", + key: "text", + }, + ], + }, + ], + }), + ); + + expect(result).toBe(`formkl "Form title (Must has)" "Form description" { + includes { + text; + } +}`); }); }); diff --git a/packages/language/src/__tests__/form-with-flattened-model.test.ts b/packages/language/src/__tests__/form-with-flattened-model.test.ts index 1b11956..e2d2eee 100644 --- a/packages/language/src/__tests__/form-with-flattened-model.test.ts +++ b/packages/language/src/__tests__/form-with-flattened-model.test.ts @@ -1,4 +1,4 @@ -import parser from "formkl"; +import parser, { defineForm } from "../"; describe("Form with flatten model", () => { it("should parse the form syntax correctly", () => { @@ -8,19 +8,46 @@ describe("Form with flatten model", () => { } }`); - expect(result).toStrictEqual({ - model: "flat", - sections: [ - { - fields: [ - { - type: "text", - label: "Text", - key: "text", - }, - ], - }, - ], - }); + expect(result).toStrictEqual( + defineForm({ + model: "flat", + sections: [ + { + fields: [ + { + type: "text", + label: "Text", + key: "text", + }, + ], + }, + ], + }), + ); + }); + + it("should stringify the form syntax correctly", () => { + const result = parser.stringify( + defineForm({ + model: "flat", + sections: [ + { + fields: [ + { + type: "text", + label: "Text", + key: "text", + }, + ], + }, + ], + }), + ); + + expect(result).toBe(`formkl flat { + includes { + text; + } +}`); }); }); diff --git a/packages/language/src/__tests__/form-with-title.test.ts b/packages/language/src/__tests__/form-with-title.test.ts index 1ae8c98..05505bc 100644 --- a/packages/language/src/__tests__/form-with-title.test.ts +++ b/packages/language/src/__tests__/form-with-title.test.ts @@ -1,4 +1,4 @@ -import parser from "formkl"; +import parser, { defineForm } from "../"; describe("Form with title", () => { it("should parse the form syntax correctly", () => { @@ -8,20 +8,48 @@ describe("Form with title", () => { } }`); - expect(result).toStrictEqual({ - model: "base", - title: "Form title", - sections: [ - { - fields: [ - { - type: "text", - label: "Text", - key: "text", - }, - ], - }, - ], - }); + expect(result).toStrictEqual( + defineForm({ + model: "base", + title: "Form title", + sections: [ + { + fields: [ + { + type: "text", + label: "Text", + key: "text", + }, + ], + }, + ], + }), + ); + }); + + it("should stringify the form syntax correctly", () => { + const result = parser.stringify( + defineForm({ + model: "base", + title: "Form title", + sections: [ + { + fields: [ + { + type: "text", + label: "Text", + key: "text", + }, + ], + }, + ], + }), + ); + + expect(result).toBe(`formkl "Form title" { + includes { + text; + } +}`); }); }); diff --git a/packages/language/src/__tests__/minimal.test.ts b/packages/language/src/__tests__/minimal.test.ts index 4bbcdfc..79cfbc4 100644 --- a/packages/language/src/__tests__/minimal.test.ts +++ b/packages/language/src/__tests__/minimal.test.ts @@ -1,4 +1,4 @@ -import parser from "formkl"; +import parser, { defineForm } from "../"; describe("Minimal test", () => { it("should parse the form syntax correctly", () => { @@ -8,19 +8,46 @@ describe("Minimal test", () => { } }`); - expect(result).toStrictEqual({ - model: "base", - sections: [ - { - fields: [ - { - type: "text", - label: "Text", - key: "text", - }, - ], - }, - ], - }); + expect(result).toStrictEqual( + defineForm({ + model: "base", + sections: [ + { + fields: [ + { + type: "text", + label: "Text", + key: "text", + }, + ], + }, + ], + }), + ); + }); + + it("should stringify the form syntax correctly", () => { + const result = parser.stringify( + defineForm({ + model: "base", + sections: [ + { + fields: [ + { + type: "text", + label: "Text", + key: "text", + }, + ], + }, + ], + }), + ); + + expect(result).toBe(`formkl { + includes { + text; + } +}`); }); }); diff --git a/packages/language/src/__tests__/multiple-field.test.ts b/packages/language/src/__tests__/multiple-field.test.ts index 55d7c3c..974b9b1 100644 --- a/packages/language/src/__tests__/multiple-field.test.ts +++ b/packages/language/src/__tests__/multiple-field.test.ts @@ -1,4 +1,4 @@ -import parser from "formkl"; +import parser, { defineForm } from "../"; describe("Multiple fields in a section", () => { it("should parse the form syntax correctly", () => { @@ -9,25 +9,58 @@ describe("Multiple fields in a section", () => { } }`); - expect(result).toStrictEqual({ - model: "base", - sections: [ - { - fields: [ - { - type: "text", - label: "Text", - key: "text", - }, - { - type: "text", - label: "Another text", - key: "another-text", - }, - ], - }, - ], - }); + expect(result).toStrictEqual( + defineForm({ + model: "base", + sections: [ + { + fields: [ + { + type: "text", + label: "Text", + key: "text", + }, + { + type: "text", + label: "Another text", + key: "another-text", + }, + ], + }, + ], + }), + ); + }); + + it("should stringify the form syntax correctly", () => { + const result = parser.stringify( + defineForm({ + model: "base", + sections: [ + { + fields: [ + { + type: "text", + label: "Text", + key: "text", + }, + { + type: "text", + label: "Another text", + key: "another-text", + }, + ], + }, + ], + }), + ); + + expect(result).toBe(`formkl { + includes { + text; + "Another text" text; + } +}`); }); it("should emit syntax error for duplicated field key", () => { diff --git a/packages/language/src/__tests__/multiple-section.test.ts b/packages/language/src/__tests__/multiple-section.test.ts index e077566..37842c3 100644 --- a/packages/language/src/__tests__/multiple-section.test.ts +++ b/packages/language/src/__tests__/multiple-section.test.ts @@ -1,4 +1,4 @@ -import parser from "formkl"; +import parser, { defineForm } from "../"; describe("Multiple section in one form", () => { it("should parse the form syntax correctly", () => { @@ -11,31 +11,72 @@ describe("Multiple section in one form", () => { } }`); - expect(result).toStrictEqual({ - model: "base", - sections: [ - { - title: "Personal information", - key: "personal-information", - fields: [ - { - type: "text", - label: "Text", - key: "text", - }, - ], - }, - { - fields: [ - { - type: "text", - label: "Text", - key: "text", - }, - ], - }, - ], - }); + expect(result).toStrictEqual( + defineForm({ + model: "base", + sections: [ + { + title: "Personal information", + key: "personal-information", + fields: [ + { + type: "text", + label: "Text", + key: "text", + }, + ], + }, + { + fields: [ + { + type: "text", + label: "Text", + key: "text", + }, + ], + }, + ], + }), + ); + }); + + it("should stringify the form syntax correctly", () => { + const result = parser.stringify( + defineForm({ + model: "base", + sections: [ + { + title: "Personal information", + key: "personal-information", + fields: [ + { + type: "text", + label: "Text", + key: "text", + }, + ], + }, + { + fields: [ + { + type: "text", + label: "Text", + key: "text", + }, + ], + }, + ], + }), + ); + + expect(result).toBe(`formkl { + "Personal information" includes { + text; + } + includes { + text; + } +}`); }); it("should emit syntax error for duplicated section key", () => { diff --git a/packages/language/src/__tests__/section-multiple-response.test.ts b/packages/language/src/__tests__/section-multiple-response.test.ts index 041923c..a84b45b 100644 --- a/packages/language/src/__tests__/section-multiple-response.test.ts +++ b/packages/language/src/__tests__/section-multiple-response.test.ts @@ -1,4 +1,4 @@ -import parser from "formkl"; +import parser, { defineForm } from "../"; describe("Section with multiple responses support", () => { it("should parse the form syntax correctly", () => { @@ -8,21 +8,49 @@ describe("Section with multiple responses support", () => { } }`); - expect(result).toStrictEqual({ - model: "base", - sections: [ - { - multiple: true, - fields: [ - { - type: "text", - label: "Text", - key: "text", - }, - ], - }, - ], - }); + expect(result).toStrictEqual( + defineForm({ + model: "base", + sections: [ + { + multiple: true, + fields: [ + { + type: "text", + label: "Text", + key: "text", + }, + ], + }, + ], + }), + ); + }); + + it("should stringify the form syntax correctly", () => { + const result = parser.stringify( + defineForm({ + model: "base", + sections: [ + { + multiple: true, + fields: [ + { + type: "text", + label: "Text", + key: "text", + }, + ], + }, + ], + }), + ); + + expect(result).toBe(`formkl { + multiple includes { + text; + } +}`); }); it("should emit syntax error for multiple response field in multiple response section.", () => { diff --git a/packages/language/src/__tests__/section-with-alias.test.ts b/packages/language/src/__tests__/section-with-alias.test.ts index 0b46988..6b23423 100644 --- a/packages/language/src/__tests__/section-with-alias.test.ts +++ b/packages/language/src/__tests__/section-with-alias.test.ts @@ -1,4 +1,4 @@ -import parser from "formkl"; +import parser, { defineForm } from "../"; describe("Section with alias", () => { it("should parse the form syntax correctly", () => { @@ -11,44 +11,86 @@ describe("Section with alias", () => { } as "different-section" }`); - expect(result).toStrictEqual({ - model: "base", - sections: [ - { - title: "Personal information", - key: "personal-information", - fields: [ - { - type: "text", - label: "Text", - key: "text", - }, - ], - }, - { - key: "different-section", - fields: [ - { - type: "text", - label: "Text", - key: "text", - }, - ], - }, - ], - }); + expect(result).toStrictEqual( + defineForm({ + model: "base", + sections: [ + { + title: "Personal information", + key: "personal-information", + fields: [ + { + type: "text", + label: "Text", + key: "text", + }, + ], + }, + { + key: "different-section", + fields: [ + { + type: "text", + label: "Text", + key: "text", + }, + ], + }, + ], + }), + ); + }); + + it("should stringify the form syntax correctly", () => { + const result = parser.stringify( + defineForm({ + model: "base", + sections: [ + { + title: "Personal information", + key: "personal-information", + fields: [ + { + type: "text", + label: "Text", + key: "text", + }, + ], + }, + { + key: "different-section", + fields: [ + { + type: "text", + label: "Text", + key: "text", + }, + ], + }, + ], + }), + ); + + expect(result).toBe(`formkl { + "Personal information" includes { + text; + } + includes { + text; + } as "different-section" +}`); }); it("should emit syntax error for duplicated section key", () => { expect(() => parser.parse(`formkl { - includes { - text; - } as "duplicated-section" - includes { - text; - } as "duplicated-section" - }`), + includes { + text; + } as "duplicated-section" + includes { + text; + } as "duplicated-section" + }`), ).toThrowError(/Duplicate section key "duplicated-section"/); }); }); diff --git a/packages/language/src/__tests__/section-with-title.test.ts b/packages/language/src/__tests__/section-with-title.test.ts index dd58a3e..2602691 100644 --- a/packages/language/src/__tests__/section-with-title.test.ts +++ b/packages/language/src/__tests__/section-with-title.test.ts @@ -1,4 +1,4 @@ -import parser from "formkl"; +import parser, { defineForm } from "../"; describe("Section with title", () => { it("should parse the form syntax correctly", () => { @@ -8,21 +8,50 @@ describe("Section with title", () => { } }`); - expect(result).toStrictEqual({ - model: "base", - sections: [ - { - title: "Personal Information", - key: "personal-information", - fields: [ - { - type: "text", - label: "Fullname", - key: "fullname", - }, - ], - }, - ], - }); + expect(result).toStrictEqual( + defineForm({ + model: "base", + sections: [ + { + title: "Personal Information", + key: "personal-information", + fields: [ + { + type: "text", + label: "Fullname", + key: "fullname", + }, + ], + }, + ], + }), + ); + }); + + it("should stringify the form syntax correctly", () => { + const result = parser.stringify( + defineForm({ + model: "base", + sections: [ + { + title: "Personal Information", + key: "personal-information", + fields: [ + { + type: "text", + label: "Fullname", + key: "fullname", + }, + ], + }, + ], + }), + ); + + expect(result).toBe(`formkl { + "Personal Information" includes { + "Fullname" text; + } +}`); }); }); diff --git a/packages/language/src/__tests__/uppercase-syntax.test.ts b/packages/language/src/__tests__/uppercase-syntax.test.ts index da99a44..e4b1ade 100644 --- a/packages/language/src/__tests__/uppercase-syntax.test.ts +++ b/packages/language/src/__tests__/uppercase-syntax.test.ts @@ -1,4 +1,4 @@ -import parser from "formkl"; +import parser, { defineForm } from "../"; describe("Used with UPPERCASE syntax", () => { it("should parse the form syntax correctly", () => { @@ -9,7 +9,7 @@ describe("Used with UPPERCASE syntax", () => { } }`); - expect(result).toStrictEqual({ + expect(result).toStrictEqual(defineForm({ model: "base", sections: [ { @@ -27,6 +27,6 @@ describe("Used with UPPERCASE syntax", () => { ], }, ], - }); + })); }); }); diff --git a/packages/language/src/define.ts b/packages/language/src/define.ts new file mode 100644 index 0000000..ba538ab --- /dev/null +++ b/packages/language/src/define.ts @@ -0,0 +1,24 @@ +import { + FieldDefault, + FieldSelection, + Formkl, + SchemaBase, + SchemaFlat, + Section, +} from "@formkl/shared"; + +export function defineForm(form: Formkl) { + return form; +} + +export function defineSection(section: Section) { + return section; +} + +export function defineField(field: FieldDefault | FieldSelection) { + return field; +} + +export function defineModel(model: SchemaBase | SchemaFlat) { + return model; +} diff --git a/packages/language/src/index.ts b/packages/language/src/index.ts index d4733c9..136b742 100644 --- a/packages/language/src/index.ts +++ b/packages/language/src/index.ts @@ -1,8 +1,10 @@ -import { Parser } from "./Parser"; -import { Tokenizer } from "./Tokenizer"; +import { Parser } from "./parser"; +import { Tokenizer } from "./tokenizer"; const parser = new Parser(); export default parser; +export * from "./define"; + export { Parser, Tokenizer }; diff --git a/packages/language/src/Parser.ts b/packages/language/src/parser.ts similarity index 78% rename from packages/language/src/Parser.ts rename to packages/language/src/parser.ts index 8cf1166..476e0a2 100644 --- a/packages/language/src/Parser.ts +++ b/packages/language/src/parser.ts @@ -1,8 +1,9 @@ import { Formkl, Section, FieldDefault, FieldSelection, HttpMethod } from "@formkl/shared"; -import { Tokenizer } from "./Tokenizer"; +import { Tokenizer } from "./tokenizer"; import { Token } from "./types"; import slugify from "slugify"; +import { Stringifier } from "./stringifier"; export class Parser { private _string: string; @@ -19,7 +20,7 @@ export class Parser { } /** - * Parses a string into an AST + * Parse a Formkl syntax string into Formkl object */ parse(string: string): Formkl { this._string = ""; @@ -39,6 +40,15 @@ export class Parser { return this.FormBlock(); } + /** + * Stringify a Formkl object to a Formkl syntax string + */ + stringify(formkl: Formkl) { + const stringifier = new Stringifier(); + + return stringifier.stringify(formkl); + } + /** * Main entry point. * @@ -110,7 +120,7 @@ export class Parser { */ private SectionBlockList(stopLookAhead = "}") { const sectionList = [this.SectionBlock()]; - while (this._lookahead != null && this._lookahead.type !== stopLookAhead) { + while (this._lookahead != null && this._lookahead?.type !== stopLookAhead) { sectionList.push(this.SectionBlock()); } return sectionList; @@ -180,7 +190,7 @@ export class Parser { * */ private FieldStatementList(stopLookAhead = "}") { const fieldStatementList = [this.FieldStatement()]; - while (this._lookahead !== null && this._lookahead.type !== stopLookAhead) { + while (this._lookahead !== null && this._lookahead?.type !== stopLookAhead) { fieldStatementList.push(this.FieldStatement()); } return fieldStatementList; @@ -206,19 +216,15 @@ export class Parser { } do { - const expression = { - REQUIRE: () => { + switch (this._lookahead?.type) { + case "REQUIRE": this._eat("REQUIRE"); field.required = true; - }, - MULTIPLE: () => { + break; + case "MULTIPLE": this._eat("MULTIPLE"); field.multiple = true; - }, - }[String(this._lookahead?.type)]; - - if (expression) { - Object.assign(field, expression()); + break; } } while (["REQUIRE", "MULTIPLE"].includes(String(this._lookahead?.type))); @@ -451,43 +457,70 @@ export class Parser { * ; */ private RelationalExpression() { - if (this._lookahead?.type) { - const expresion = { - OPERATOR_RELATIONAL: () => { - const operator = { - ">": "$gt", - ">=": "$gte", - "<": "$lt", - "<=": "$lte", - }[this._eat("OPERATOR_RELATIONAL").value] as string; - - return { - [operator]: this.NumericLiteral(), - }; - }, - OPERATOR_EQUALITY: () => { - const operator = { - "==": "$eq", - "!=": "$neq", - }[this._eat("OPERATOR_EQUALITY").value] as string; - - return { - [operator]: isNaN(this._lookahead?.value as number) - ? this.StringLiteral() - : this.NumericLiteral(), - }; - }, - HAS: () => { - this._eat("HAS"); - return { - $has: isNaN(this._lookahead?.value as number) - ? this.StringLiteral() - : this.NumericLiteral(), - }; - }, - }[this._lookahead.type]; - - if (expresion) return expresion(); + switch (this._lookahead?.type) { + case "OPERATOR_RELATIONAL": + const relationalOperator = this._eat("OPERATOR_RELATIONAL").value; + + switch (relationalOperator) { + case ">": + return { $gt: this.NumericLiteral() }; + case ">=": + return { $gte: this.NumericLiteral() }; + case "<": + return { $lt: this.NumericLiteral() }; + case "<=": + return { $lte: this.NumericLiteral() }; + default: + throw new SyntaxError(`Unknown relational operator: ${relationalOperator}`); + } + case "OPERATOR_EQUALITY": + const equalityOperator = this._eat("OPERATOR_EQUALITY").value; + const equalityOperatorKey = equalityOperator === "==" ? "$eq" : "$neq"; + + switch (equalityOperator) { + case "==": + case "!=": + switch ( + this._lookahead?.type as + | "NUMBER" + | "NAN" + | "NULL" + | "UNDEFINED" + | "TRUE" + | "FALSE" + | "STRING" + ) { + case "NUMBER": + return { [equalityOperatorKey]: this.NumericLiteral() }; + case "NAN": + return { [equalityOperatorKey]: this.NaNLiteral() }; + case "NULL": + return { [equalityOperatorKey]: this.NullLiteral() }; + case "UNDEFINED": + return { [equalityOperatorKey]: this.UndefinedLiteral() }; + case "TRUE": + case "FALSE": + return { + [equalityOperatorKey]: this.BooleanLiteral( + this._lookahead?.type as "TRUE" | "FALSE", + ), + }; + case "STRING": + return { [equalityOperatorKey]: this.StringLiteral() }; + default: + throw new SyntaxError(`Unknown equality value type: ${this._lookahead?.type}`); + } + default: + throw new SyntaxError(`Unknown equality operator: ${equalityOperator}`); + } + case "HAS": + this._eat("HAS"); + + return { + $has: isNaN(this._lookahead?.value as number) + ? this.StringLiteral() + : this.NumericLiteral(), + }; } } @@ -506,6 +539,16 @@ export class Parser { return strings; } + /** + * NaNLiteral + * : 'NaN' + * ; + */ + private NaNLiteral() { + this._eat("NAN"); + return NaN; + } + /* * NumericLiteral * : NUMBER @@ -532,8 +575,8 @@ export class Parser { * | FALSE * ; */ - private BooleanLiteral(value: boolean) { - this._eat(value ? "true" : "false"); + private BooleanLiteral(value: "TRUE" | "FALSE") { + this._eat(value ? "TRUE" : "FALSE"); return value; } @@ -543,10 +586,20 @@ export class Parser { * ; */ private NullLiteral() { - this._eat("null"); + this._eat("NULL"); return null; } + /** + * UndefinedLiteral + * : 'undefined' + * ; + */ + private UndefinedLiteral() { + this._eat("UNDEFINED"); + return undefined; + } + /** * Expects a token of a given type. */ diff --git a/packages/language/src/stringifier.ts b/packages/language/src/stringifier.ts new file mode 100644 index 0000000..2c64d36 --- /dev/null +++ b/packages/language/src/stringifier.ts @@ -0,0 +1,110 @@ +import { + FieldDefault, + FieldSelection, + Formkl, + Section, + Validation, + ValidationLogic, +} from "@formkl/shared"; +import slugify from "slugify"; + +export class Stringifier { + constructor() {} + + validateLogicAnd(andLogic: Validation["logic"]["$and"]): string { + const results = andLogic.map((e) => this.validationLogic(e)); + + return results.join(`%(s)and%(s)`); + } + + validateLogicOr(orLogic: Validation["logic"]["$or"]): string { + const results = orLogic.map((e) => this.validationLogic(e)); + + return results.join(`%(s)or%(s)`); + } + + validationLogic(logic: Validation["logic"]): string { + const operators = Object.keys(logic) as Array; + const operator = operators[0]; + const val = logic[operator] as string | number; + + return { + $gt: () => `>%(s)${val}`, + $lt: () => `>=%(s)${val}`, + $gteq: () => `<%(s)${val}`, + $lteq: () => `<=%(s)${val}`, + $eq: () => `==%(s)${typeof val === "string" ? JSON.stringify(val) : val}`, + $has: () => `has%(s)${typeof val === "string" ? JSON.stringify(val) : val}`, + $and: () => logic.$and && this.validateLogicAnd(logic.$and), + $or: () => logic.$or && this.validateLogicOr(logic.$or), + }[operator](); + } + + validation(validation: Validation) { + return [ + validation.regex && `regex("${validation.regex.source}")`, + validation.logic && `valid(${this.validationLogic(validation.logic)})`, + ] + .filter((i) => i) + .join("%(s)"); + } + + fields(fields: Array) { + return ( + "%(t)%(t)" + + fields + .map( + (field) => + [ + field.required && "require", + field.maxResponseAllowed ? field.maxResponseAllowed : field.multiple && "multiple", + slugify(field.label).toLowerCase() !== field.type && `"${field.label}"`, + field.type, + field.validation && this.validation(field.validation), + slugify(field.label).toLowerCase() !== field.key && `as%(s)"${field.key}"`, + ] + .filter((i) => i) + .join("%(s)") + ";", + ) + .join("%(n)%(t)%(t)") + ); + } + + sections(sections: Array
) { + return sections + .map((section) => + [ + "%(t)", + section.multiple && "multiple%(s)", + section.title && `"${section.title}"%(s)`, + "includes", + "%(s)", + "{", + "%(n)", + this.fields(section.fields), + "%(n)", + "%(t)", + "}", + (!section.title && section.key) || + (section.title && slugify(section.title).toLowerCase() !== section.key) + ? `%(s)as%(s)"${section.key}"` + : "", + ].join(""), + ) + .join("%(n)"); + } + + stringify(formkl: Formkl) { + return `${[ + "formkl", + formkl.model === "flat" && "flat", + formkl.title && JSON.stringify(formkl.title), + formkl.description && JSON.stringify(formkl.description), + ] + .filter((i) => i) + .join("%(s)")}%(s){%(n)${this.sections(formkl.sections)}%(n)}` + .replace(/\%\(s\)/g, " ") + .replace(/\%\(t\)/g, "\t") + .replace(/\%\(n\)/g, "\n"); + } +} diff --git a/packages/language/src/Tokenizer.ts b/packages/language/src/tokenizer.ts similarity index 94% rename from packages/language/src/Tokenizer.ts rename to packages/language/src/tokenizer.ts index 12948f5..aa6a039 100644 --- a/packages/language/src/Tokenizer.ts +++ b/packages/language/src/tokenizer.ts @@ -93,6 +93,14 @@ const Specs: Array = [ // Single quoted String: [/^'[^']*'/, "STRING"], + // -------------------------------------- + // Values by Keyword: + [createKeywordRegex("NaN"), "NAN"], + [createKeywordRegex("FALSE"), "FALSE"], + [createKeywordRegex("TRUE"), "TRUE"], + [createKeywordRegex("NULL"), "NULL"], + [createKeywordRegex("UNDEFINED"), "UNDEFINED"], + // -------------------------------------- // Identifier [/^\w+/, "IDENTIFIER"], diff --git a/packages/language/src/utils/createKeywordRegex.ts b/packages/language/src/utils/createKeywordRegex.ts index 067d9e1..12a3d44 100644 --- a/packages/language/src/utils/createKeywordRegex.ts +++ b/packages/language/src/utils/createKeywordRegex.ts @@ -1,6 +1,7 @@ export const createKeywordRegex = (keyword: string) => { return new RegExp( `^\\b(${[ + keyword, keyword.toLowerCase(), keyword.toUpperCase(), keyword.toLowerCase().charAt(0).toUpperCase() + keyword.toLowerCase().slice(1), diff --git a/packages/plugins/vue/package.json b/packages/plugins/vue/package.json index 4165bc8..a104918 100644 --- a/packages/plugins/vue/package.json +++ b/packages/plugins/vue/package.json @@ -1,6 +1,6 @@ { "name": "@formkl/plugin-vue", - "version": "1.0.0-rc.2", + "version": "1.0.0-rc.3", "description": "A Formkl components plugin to register components that used in @formkl/vue adapter", "type": "module", "main": "dist/index.es.js", diff --git a/packages/shared/package.json b/packages/shared/package.json index 1858d6c..b72ed6d 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -1,6 +1,6 @@ { "name": "@formkl/shared", - "version": "1.0.0-rc.2", + "version": "1.0.0-rc.3", "type": "module", "main": "./src/index.ts", "scripts": { diff --git a/packages/shared/src/__tests__/with-parser/validate-logic-and-n-or.test.ts b/packages/shared/src/__tests__/with-parser/validate-logic-and-n-or.test.ts index 797115b..cbf5628 100644 --- a/packages/shared/src/__tests__/with-parser/validate-logic-and-n-or.test.ts +++ b/packages/shared/src/__tests__/with-parser/validate-logic-and-n-or.test.ts @@ -1,6 +1,6 @@ import { isValueValidated } from "@formkl/shared"; -import parser from "formkl"; +import parser from "../../../../language"; describe("Test recursive validator", () => { it("should return true", () => { diff --git a/packages/shared/src/__tests__/with-parser/validate-logic-and.test.ts b/packages/shared/src/__tests__/with-parser/validate-logic-and.test.ts index cf9cddb..0a4566f 100644 --- a/packages/shared/src/__tests__/with-parser/validate-logic-and.test.ts +++ b/packages/shared/src/__tests__/with-parser/validate-logic-and.test.ts @@ -1,6 +1,6 @@ import { isValueValidated } from "@formkl/shared"; -import parser from "formkl"; +import parser from "../../../../language"; describe("Test recursive validator", () => { it("should return false", () => { diff --git a/packages/shared/src/__tests__/with-parser/validate-logic-gt.test.ts b/packages/shared/src/__tests__/with-parser/validate-logic-gt.test.ts index 79a5ccb..4e2db66 100644 --- a/packages/shared/src/__tests__/with-parser/validate-logic-gt.test.ts +++ b/packages/shared/src/__tests__/with-parser/validate-logic-gt.test.ts @@ -1,6 +1,6 @@ import { isValueValidated } from "@formkl/shared"; -import parser from "formkl"; +import parser from "../../../../language"; describe("Test recursive validator", () => { it("should return true", () => { diff --git a/packages/shared/src/__tests__/with-parser/validate-logic-lt.test.ts b/packages/shared/src/__tests__/with-parser/validate-logic-lt.test.ts index e273e0a..2f0e3ed 100644 --- a/packages/shared/src/__tests__/with-parser/validate-logic-lt.test.ts +++ b/packages/shared/src/__tests__/with-parser/validate-logic-lt.test.ts @@ -1,6 +1,6 @@ import { isValueValidated } from "@formkl/shared"; -import parser from "formkl"; +import parser from "../../../../language"; describe("Test recursive validator", () => { it("should return false", () => { diff --git a/packages/shared/src/__tests__/with-parser/validate-logic-n-regex.test.ts b/packages/shared/src/__tests__/with-parser/validate-logic-n-regex.test.ts index 6fb6336..26ae5d8 100644 --- a/packages/shared/src/__tests__/with-parser/validate-logic-n-regex.test.ts +++ b/packages/shared/src/__tests__/with-parser/validate-logic-n-regex.test.ts @@ -1,6 +1,6 @@ import { isValueValidated } from "@formkl/shared"; -import parser from "formkl"; +import parser from "../../../../language"; describe("Test logic and regex", () => { it("should return false", () => { diff --git a/packages/shared/src/__tests__/with-parser/validate-logic-or.test.ts b/packages/shared/src/__tests__/with-parser/validate-logic-or.test.ts index 33660d1..344a286 100644 --- a/packages/shared/src/__tests__/with-parser/validate-logic-or.test.ts +++ b/packages/shared/src/__tests__/with-parser/validate-logic-or.test.ts @@ -1,6 +1,6 @@ import { isValueValidated } from "@formkl/shared"; -import parser from "formkl"; +import parser from "../../../../language"; describe("Test recursive validator", () => { it("should return true", () => { diff --git a/packages/shared/src/__tests__/with-parser/validate-regex.test.ts b/packages/shared/src/__tests__/with-parser/validate-regex.test.ts index 6243999..fc10fe2 100644 --- a/packages/shared/src/__tests__/with-parser/validate-regex.test.ts +++ b/packages/shared/src/__tests__/with-parser/validate-regex.test.ts @@ -1,6 +1,6 @@ import { isValueValidated } from "@formkl/shared"; -import parser from "formkl"; +import parser from "../../../../language"; describe("Test recursive validator", () => { it("should return true", () => { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b7e7edd..cec5fe3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -26,6 +26,7 @@ importers: nx: ^15.0.0 release-it: ^15.5.0 rimraf: ^3.0.2 + ts-node: ^10.9.1 typescript: ^4.8.4 vitepress: 1.0.0-alpha.20 vitest: ^0.24.4 @@ -56,6 +57,7 @@ importers: nx: 15.0.0 release-it: 15.5.0 rimraf: 3.0.2 + ts-node: 10.9.1_7jzoohtnaegavowzoeccrsbhty typescript: 4.8.4 vitest: 0.24.4_dof64qmg5cgec6bfb7aeo2w7mu vue: 3.2.41 @@ -1183,7 +1185,7 @@ packages: lodash.merge: 4.6.2 lodash.uniq: 4.5.0 resolve-from: 5.0.0 - ts-node: 10.9.1_7jzoohtnaegavowzoeccrsbhty + ts-node: 10.9.1_yodorn5kzjgomblrsstrk2spaa typescript: 4.8.4 transitivePeerDependencies: - '@swc/core' @@ -3847,7 +3849,7 @@ packages: dependencies: '@types/node': 14.18.33 cosmiconfig: 7.1.0 - ts-node: 10.9.1_7jzoohtnaegavowzoeccrsbhty + ts-node: 10.9.1_yodorn5kzjgomblrsstrk2spaa typescript: 4.8.4 dev: true @@ -8763,6 +8765,37 @@ packages: yn: 3.1.1 dev: true + /ts-node/10.9.1_yodorn5kzjgomblrsstrk2spaa: + resolution: {integrity: sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==} + hasBin: true + peerDependencies: + '@swc/core': '>=1.2.50' + '@swc/wasm': '>=1.2.50' + '@types/node': '*' + typescript: '>=2.7' + peerDependenciesMeta: + '@swc/core': + optional: true + '@swc/wasm': + optional: true + dependencies: + '@cspotcode/source-map-support': 0.8.1 + '@tsconfig/node10': 1.0.9 + '@tsconfig/node12': 1.0.11 + '@tsconfig/node14': 1.0.3 + '@tsconfig/node16': 1.0.3 + '@types/node': 14.18.33 + acorn: 8.8.0 + acorn-walk: 8.2.0 + arg: 4.1.3 + create-require: 1.1.1 + diff: 4.0.2 + make-error: 1.3.6 + typescript: 4.8.4 + v8-compile-cache-lib: 3.0.1 + yn: 3.1.1 + dev: true + /tsconfig-paths/3.14.1: resolution: {integrity: sha512-fxDhWnFSLt3VuTwtvJt5fpwxBHg5AdKWMsgcPOOIilyjymcYVZoCQF8fvFRezCNfblEXmi+PcM1eYHeOAgXCOQ==} dependencies: