├── .nvmrc ├── example ├── env.d.ts ├── tsconfig.json ├── tsconfig.app.json ├── src │ ├── main.ts │ └── App.vue ├── vite.config.ts ├── index.html ├── tsconfig.node.json ├── .gitignore ├── package.json └── pnpm-lock.yaml ├── .gitattributes ├── .eslintrc.json ├── .vscode ├── settings.json └── extensions.json ├── demo-i18n.png ├── demo-async.png ├── demo-debug.png ├── docs ├── editor.png ├── .vitepress │ ├── components │ │ └── DemoContainer.vue │ ├── theme │ │ └── index.js │ └── config.js ├── editor-demo.md ├── executor-demo.md ├── ExecutorDemo.vue ├── EditorDemo.vue └── index.md ├── src ├── utils │ ├── index.ts │ └── basic.ts ├── components │ ├── index.ts │ ├── Debug.vue │ ├── CmWrap.vue │ └── Editor.vue ├── processes │ ├── index.ts │ ├── funcLib.ts │ ├── executor.ts │ ├── interface.ts │ └── cmEditor.ts ├── assets │ ├── icon.ts │ ├── main.css │ └── locales │ │ ├── zh-CN.json │ │ └── en.json ├── env.d.ts ├── index.ts └── locales.ts ├── demo-diagnostic.png ├── .prettierignore ├── .gitignore ├── postcss.config.js ├── eslint.config.js ├── .prettierrc.json ├── typedoc.json ├── tailwind.config.js ├── components.d.ts ├── .github └── workflows │ ├── leak_checker.yml │ └── CICD.yml ├── tsconfig.json ├── vite.config.ts ├── LICENSE ├── README.md ├── package.json └── test ├── executor.test.ts └── cmEditor.test.ts /.nvmrc: -------------------------------------------------------------------------------- 1 | lts/* 2 | -------------------------------------------------------------------------------- /example/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.sh eol=lf 2 | Dockerfile eol=lf -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "prettier/prettier": "off" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "vue-i18n.i18nPaths": "src\\assets\\locales" 3 | } 4 | -------------------------------------------------------------------------------- /demo-i18n.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ideal-world/formula-editor/HEAD/demo-i18n.png -------------------------------------------------------------------------------- /demo-async.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ideal-world/formula-editor/HEAD/demo-async.png -------------------------------------------------------------------------------- /demo-debug.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ideal-world/formula-editor/HEAD/demo-debug.png -------------------------------------------------------------------------------- /docs/editor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ideal-world/formula-editor/HEAD/docs/editor.png -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | import * as IwUtils from './basic' 2 | 3 | export { 4 | IwUtils, 5 | } 6 | -------------------------------------------------------------------------------- /demo-diagnostic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ideal-world/formula-editor/HEAD/demo-diagnostic.png -------------------------------------------------------------------------------- /src/components/index.ts: -------------------------------------------------------------------------------- 1 | import IwEditor from './Editor.vue' 2 | 3 | export { 4 | IwEditor, 5 | } 6 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | /dist/* 2 | .local 3 | .output.js 4 | /node_modules/** 5 | 6 | **/*.svg 7 | **/*.sh 8 | 9 | /public/* -------------------------------------------------------------------------------- /docs/.vitepress/components/DemoContainer.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | dist 4 | dist-ssr 5 | *.local 6 | docs/.vitepress/cache 7 | docs/.vitepress/.temp 8 | docs/api 9 | types 10 | .idea -------------------------------------------------------------------------------- /src/processes/index.ts: -------------------------------------------------------------------------------- 1 | import * as iwInterface from './interface' 2 | import * as iwExecutor from './executor' 3 | 4 | export { 5 | iwInterface, 6 | iwExecutor, 7 | } 8 | -------------------------------------------------------------------------------- /src/assets/icon.ts: -------------------------------------------------------------------------------- 1 | // https://primer.style/foundations/icons 2 | 3 | export const DEBUG = 'octicon-bug-24' 4 | export const INFO = 'octicon-code-review-24' 5 | export const RUN = 'octicon-play-24' 6 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | 'postcss-import': {}, 4 | 'tailwindcss/nesting': 'postcss-nesting', 5 | 'tailwindcss': {}, 6 | 'autoprefixer': {}, 7 | }, 8 | } 9 | -------------------------------------------------------------------------------- /example/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "references": [ 3 | { 4 | "path": "./tsconfig.node.json" 5 | }, 6 | { 7 | "path": "./tsconfig.app.json" 8 | } 9 | ], 10 | "files": [] 11 | } 12 | -------------------------------------------------------------------------------- /docs/editor-demo.md: -------------------------------------------------------------------------------- 1 | 4 | 5 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | declare module '*.vue' { 4 | import type { DefineComponent } from 'vue' 5 | 6 | const component: DefineComponent<{}, {}, any> 7 | export default component 8 | } 9 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "vue.volar", 4 | "dbaeumer.vscode-eslint", 5 | "bradlc.vscode-tailwindcss", 6 | "ms-vscode.vscode-typescript-next", 7 | "vitest.explorer" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import antfu from '@antfu/eslint-config' 2 | 3 | export default antfu({ 4 | overrides: { 5 | vue: { 6 | 'vue/no-mutating-props': ['error', { 7 | shallowOnly: true, 8 | }], 9 | }, 10 | }, 11 | }) 12 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/prettierrc", 3 | "semi": false, 4 | "tabWidth": 2, 5 | "useTabs": false, 6 | "singleQuote": true, 7 | "printWidth": 180, 8 | "trailingComma": "all", 9 | "embeddedLanguageFormatting": "auto" 10 | } 11 | -------------------------------------------------------------------------------- /example/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@vue/tsconfig/tsconfig.dom.json", 3 | "compilerOptions": { 4 | "composite": true, 5 | "baseUrl": ".", 6 | "paths": { 7 | "@/*": ["./src/*"] 8 | } 9 | }, 10 | "include": ["env.d.ts", "src/**/*", "src/**/*.vue"], 11 | "exclude": ["src/**/__tests__/*"] 12 | } 13 | -------------------------------------------------------------------------------- /example/src/main.ts: -------------------------------------------------------------------------------- 1 | import FormulaEditor from '@idealworld/formula-editor' 2 | import '@idealworld/formula-editor/dist/style.css' 3 | import ElementPlus from 'element-plus' 4 | import 'element-plus/dist/index.css' 5 | import { createApp } from 'vue' 6 | import App from './App.vue' 7 | 8 | createApp(App) 9 | .use(ElementPlus) 10 | .use(FormulaEditor) 11 | .mount('#app') 12 | -------------------------------------------------------------------------------- /typedoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugin": ["typedoc-plugin-markdown", "typedoc-vitepress-theme"], 3 | "entryFileName": "exports.md", 4 | "entryPoints": ["src/index.ts"], 5 | "includes": "src/*.ts", 6 | "readme": "none", 7 | "includeVersion": true, 8 | "disableSources": false, 9 | "sidebar": { 10 | "autoConfiguration": false 11 | }, 12 | "out":"./docs/api" 13 | } 14 | -------------------------------------------------------------------------------- /example/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { URL, fileURLToPath } from 'node:url' 2 | 3 | import { defineConfig } from 'vite' 4 | import vue from '@vitejs/plugin-vue' 5 | 6 | // https://vitejs.dev/config/ 7 | export default defineConfig({ 8 | plugins: [ 9 | vue(), 10 | ], 11 | resolve: { 12 | alias: { 13 | '@': fileURLToPath(new URL('./src', import.meta.url)), 14 | }, 15 | }, 16 | }) 17 | -------------------------------------------------------------------------------- /example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Formula Example 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | import daisyui from 'daisyui' 3 | 4 | export default { 5 | content: ['./src/**/*.{vue,html,js}'], 6 | theme: { 7 | extend: {}, 8 | }, 9 | plugins: [daisyui], 10 | daisyui: { 11 | prefix: 'iw-', 12 | themes: ['light', 'dark', 'cupcake', 'forest', 'lofi', 'black', 'acid', 'lemonade', 'night', 'coffee'], 13 | }, 14 | } 15 | -------------------------------------------------------------------------------- /docs/.vitepress/theme/index.js: -------------------------------------------------------------------------------- 1 | import DefaultTheme from 'vitepress/theme' 2 | import DemoContainer from '../components/DemoContainer.vue' 3 | import FormulaEditor from '../../../src' 4 | 5 | globalThis.__VUE_PROD_DEVTOOLS__ = false 6 | 7 | export default { 8 | ...DefaultTheme, 9 | enhanceApp({ app }) { 10 | app.use(FormulaEditor) 11 | app.component('DemoContainer', DemoContainer) 12 | }, 13 | } 14 | -------------------------------------------------------------------------------- /docs/executor-demo.md: -------------------------------------------------------------------------------- 1 | 4 | 5 | This component provides an independent formula execution engine. 6 | 7 | For the API of the execution engine, see: [Execute API](api/namespaces/iwExecutor/functions/execute.html) 8 | 9 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /example/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/node18/tsconfig.json", 3 | "compilerOptions": { 4 | "composite": true, 5 | "module": "ESNext", 6 | "moduleResolution": "Bundler", 7 | "types": ["node"] 8 | }, 9 | "include": [ 10 | "vite.config.*", 11 | "vitest.config.*", 12 | "cypress.config.*", 13 | "nightwatch.conf.*", 14 | "playwright.config.*" 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /example/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | .DS_Store 12 | dist 13 | dist-ssr 14 | coverage 15 | *.local 16 | package-lock.json 17 | 18 | /cypress/videos/ 19 | /cypress/screenshots/ 20 | 21 | # Editor directories and files 22 | .vscode/* 23 | !.vscode/extensions.json 24 | .idea 25 | *.suo 26 | *.ntvs* 27 | *.njsproj 28 | *.sln 29 | *.sw? 30 | -------------------------------------------------------------------------------- /components.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /* prettier-ignore */ 3 | // @ts-nocheck 4 | // Generated by unplugin-vue-components 5 | // Read more: https://github.com/vuejs/core/pull/3399 6 | export {} 7 | 8 | declare module 'vue' { 9 | export interface GlobalComponents { 10 | CmWrap: typeof import('./src/components/CmWrap.vue')['default'] 11 | Debug: typeof import('./src/components/Debug.vue')['default'] 12 | Editor: typeof import('./src/components/Editor.vue')['default'] 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import type { App } from 'vue' 2 | import { defineCustomElement } from 'vue' 3 | import './assets/main.css' 4 | import locales from './locales' 5 | import { IwEditor } from './components' 6 | 7 | export default (app: App): void => { 8 | app.use(locales).component('IwEditor', IwEditor) 9 | } 10 | 11 | export const IwEditorComp = defineCustomElement(IwEditor) 12 | 13 | declare module 'vue' { 14 | export interface GlobalComponents { 15 | 'IwEditor': typeof IwEditorComp 16 | } 17 | } 18 | 19 | export * from './processes' 20 | -------------------------------------------------------------------------------- /src/locales.ts: -------------------------------------------------------------------------------- 1 | import { createI18n } from 'vue-i18n' 2 | import en from './assets/locales/en.json' 3 | import zh from './assets/locales/zh-CN.json' 4 | 5 | const locales = createI18n({ 6 | legacy: false, 7 | locale: (typeof localStorage !== 'undefined' ? localStorage.getItem('locale') : undefined) || (typeof navigator !== 'undefined' && typeof navigator.language !== 'undefined' ? navigator.language.slice(0, 2) : undefined), 8 | fallbackLocale: 'en', 9 | messages: { 10 | zh, 11 | en, 12 | }, 13 | }) 14 | 15 | export default locales 16 | -------------------------------------------------------------------------------- /.github/workflows/leak_checker.yml: -------------------------------------------------------------------------------- 1 | name: LeakChecker 2 | 3 | on: [push, pull_request_target] 4 | 5 | jobs: 6 | hello_world_job: 7 | runs-on: ubuntu-latest 8 | name: Scan Keywords 9 | steps: 10 | - name: Checkout 11 | uses: actions/checkout@v3 12 | - name: Scan Keywords 13 | uses: PPG007/keywords-scanner@v1.0 14 | with: 15 | keywords: ${{secrets.LEAK_WORDS}} 16 | ignoreCase: true 17 | ignoredDirs: '["docs","pnpm-lock.yaml","node_modules","demo-diagnostic.png","demo-debug.png","demo-async.png","demo-i18n.png"]' 18 | -------------------------------------------------------------------------------- /docs/.vitepress/config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | title: 'Formula Editor', 3 | description: 'A relatively complete formula editor', 4 | base: '/formula-editor/', 5 | themeConfig: { 6 | nav: [ 7 | { text: 'Introduction', link: '/' }, 8 | { text: 'Edit Demo', link: '/editor-demo' }, 9 | { text: 'Execute Demo', link: '/executor-demo' }, 10 | { text: 'API', link: '/api/exports' }, 11 | ], 12 | socialLinks: [{ icon: 'github', link: 'https://github.com/ideal-world/formula-editor' }], 13 | }, 14 | vite: { 15 | build: { 16 | chunkSizeWarningLimit: 1500, 17 | }, 18 | }, 19 | } 20 | -------------------------------------------------------------------------------- /src/assets/main.css: -------------------------------------------------------------------------------- 1 | @import 'octicons-css/octicons.min.css'; 2 | 3 | @tailwind base; 4 | @tailwind components; 5 | @tailwind utilities; 6 | 7 | /* Eliminating the effects of vitepress */ 8 | .iw-editor { 9 | 10 | button, 11 | input, 12 | optgroup, 13 | select, 14 | textarea { 15 | border-width: 1px; 16 | border-style: solid; 17 | } 18 | 19 | a { 20 | text-decoration: none !important; 21 | } 22 | 23 | p { 24 | line-height: 1; 25 | } 26 | 27 | p, 28 | summary { 29 | margin: 0; 30 | } 31 | 32 | ul { 33 | margin: 0; 34 | } 35 | 36 | li+li { 37 | margin-top: 0; 38 | } 39 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "lib": [ 5 | "esnext", 6 | "dom" 7 | ], 8 | "useDefineForClassFields": true, 9 | "module": "esnext", 10 | "moduleResolution": "node", 11 | "resolveJsonModule": true, 12 | "types": [ 13 | "vite/client" 14 | ], 15 | "strict": true, 16 | "declaration": true, 17 | "declarationDir": "./dist/types", 18 | "emitDeclarationOnly": true, 19 | "sourceMap": true, 20 | "esModuleInterop": true, 21 | "isolatedModules": true, 22 | "skipLibCheck": true 23 | }, 24 | "include": [ 25 | "src/**/*.ts", 26 | "src/**/*.d.ts", 27 | "src/**/*.vue", 28 | "src/index.js" 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "formula-example", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "vite", 7 | "build": "run-p type-check \"build-only {@}\" --", 8 | "preview": "vite preview", 9 | "build-only": "vite build", 10 | "type-check": "vue-tsc --noEmit -p tsconfig.app.json --composite false" 11 | }, 12 | "dependencies": { 13 | "@element-plus/icons-vue": "^2.3.1", 14 | "@idealworld/formula-editor": "file:../", 15 | "element-plus": "^2.4.3", 16 | "vue": "^3.3.11" 17 | }, 18 | "devDependencies": { 19 | "@tsconfig/node18": "^18.2.2", 20 | "@types/node": "^20.10.4", 21 | "@vitejs/plugin-vue": "^4.5.2", 22 | "@vue/tsconfig": "^0.4.0", 23 | "npm-run-all2": "^6.1.1", 24 | "typescript": "^5.3.3", 25 | "vite": "^5.0.8", 26 | "vue-tsc": "^1.8.25" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'node:path' 2 | import { defineConfig } from 'vite' 3 | import Components from 'unplugin-vue-components/vite' 4 | import vue from '@vitejs/plugin-vue' 5 | 6 | export default defineConfig({ 7 | plugins: [ 8 | vue(), 9 | Components({ 10 | dirs: ['src/components'], 11 | extensions: ['vue'], 12 | dts: 'components.d.ts', 13 | }), 14 | ], 15 | build: { 16 | lib: { 17 | entry: path.resolve(__dirname, 'src/index.ts'), 18 | name: 'formula-editor', 19 | formats: ['es', 'umd'], 20 | fileName: format => `formula-editor.${format}.js`, 21 | }, 22 | rollupOptions: { 23 | external: ['vue', 'vue-i18n', 'codemirror', 'octicons-css', 'eslint-linter-browserify', 'vue-codemirror6', /@codemirror\/.+/, /@lezer\/.+/], 24 | output: { 25 | exports: 'named', 26 | globals: { 27 | vue: 'Vue', 28 | }, 29 | }, 30 | }, 31 | emptyOutDir: false, 32 | }, 33 | test: { 34 | globals: true, 35 | environment: 'happy-dom', 36 | }, 37 | }) 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Ideal World 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Formula Editor 2 | 3 | [![NPM](https://img.shields.io/npm/v/%40idealworld%2Fformula-editor)](https://www.npmjs.com/package/@idealworld/formula-editor) 4 | [![GitHub](https://img.shields.io/github/license/ideal-world/formula-editor)](https://github.com/ideal-world/formula-editor) 5 | [![Last Commit](https://img.shields.io/github/last-commit/ideal-world/formula-editor)](https://github.com/ideal-world/formula-editor/commits/main) 6 | [![Build Status](https://github.com/ideal-world/formula-editor/actions/workflows/CICD.yml/badge.svg?branch=main)](https://github.com/ideal-world/formula-editor/actions/workflows/CICD.yml) 7 | 8 | **A relatively complete formula editor.** 9 | 10 | ## Features 11 | 12 | - Visual design 13 | - Relatively comprehensive error prompts 14 | - Auto-completion prompts 15 | - Customizable variables and synchronous/asynchronous functions 16 | - Online debugging 17 | - Dynamic formula execution engine 18 | 19 | ## Screenshots 20 | 21 | **Error diagnosis** 22 | 23 | ![demo-diagnostic.png](demo-diagnostic.png) 24 | 25 | **Online debugging** 26 | 27 | ![demo-debug.png](demo-debug.png) 28 | 29 | **Asynchronous call** 30 | 31 | ![demo-async.png](demo-async.png) 32 | 33 | **I18n** 34 | 35 | ![demo-i18n.png](demo-i18n.png) 36 | 37 | ## Documentation and examples 38 | 39 | [https://ideal-world.github.io/formula-editor/](https://ideal-world.github.io/formula-editor/) 40 | -------------------------------------------------------------------------------- /src/utils/basic.ts: -------------------------------------------------------------------------------- 1 | export function groupBy(array: T[], predicate: (value: T, index: number, array: T[]) => string) { 2 | return array.reduce((acc, value, index, array) => { 3 | (acc[predicate(value, index, array)] ||= []).push(value) 4 | return acc 5 | }, {} as { [key: string]: T[] }) 6 | } 7 | 8 | export function hasParentWithClass(element: HTMLElement | null, className: string): boolean { 9 | return getParentWithClass(element, className) != null 10 | } 11 | 12 | export function getParentWithClass(element: HTMLElement | null, className: string): HTMLElement | null { 13 | while (element) { 14 | if (element.classList && element.classList.contains(className)) 15 | return element 16 | 17 | element = element.parentElement 18 | } 19 | return null 20 | } 21 | 22 | export function getChildIndex(parent: HTMLElement, child: HTMLElement): number { 23 | return Array.prototype.indexOf.call(parent.children, child) 24 | } 25 | 26 | export function getRandomInt(min: number, max: number) { 27 | min = Math.ceil(min) 28 | max = Math.floor(max) 29 | return Math.floor(Math.random() * (max - min + 1)) + min 30 | } 31 | 32 | export function getRandomString(length: number) { 33 | let result = '' 34 | const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789' 35 | for (let i = 0; i < length; i++) 36 | result += characters.charAt(Math.floor(Math.random() * characters.length)) 37 | 38 | return result 39 | } 40 | -------------------------------------------------------------------------------- /src/assets/locales/zh-CN.json: -------------------------------------------------------------------------------- 1 | { 2 | "_": { 3 | "var": "变量", 4 | "fun": "函数" 5 | }, 6 | "data_kind": { 7 | "STRING": "字符串", 8 | "NUMBER": "数值", 9 | "BOOLEAN": "布尔", 10 | "NULL": "空", 11 | "STRINGS": "字符串数组", 12 | "NUMBERS": "数值数组", 13 | "BOOLEANS": "布尔数组", 14 | "ANY": "任意类型" 15 | }, 16 | "diagnostic": { 17 | "unterminated_string_constant": "未终止的字符串常量", 18 | "syntax_error": "公式语法错误", 19 | "format_error": "期望格式为[{expect}],实际为[{real}]", 20 | "bracket_error": "括号格式错误", 21 | "binary_expression_error": "二元表达式格式错误", 22 | "conditional_expression_error": "条件表达式格式错误", 23 | "var_fun_not_exist_error": "变量/函数不存在", 24 | "var_not_exist_error": "变量不存在", 25 | "fun_not_exist_error": "函数不存在", 26 | "param_not_exist_error": "缺少参数", 27 | "fun_format_error": "函数格式错误", 28 | "namespace_not_exist_error": "命名空间不存在", 29 | "param_length_error": "期望参数长度为[{expect}],实际为[{real}]" 30 | }, 31 | "executor": { 32 | "param_value_not_exist_error": "参数 [{var}] 值不存在", 33 | "execute_error": "公式执行错误:" 34 | }, 35 | "editor": { 36 | "placeholder": "在此输入公式", 37 | "debug": "调试", 38 | "search_var": "搜索变量", 39 | "search_fun": "搜索函数/API", 40 | "empty": "暂无数据", 41 | "tips": "小提示", 42 | "tip1": "指向右侧函数时可在此处显示使用说明", 43 | "tip2": "点击右侧函数可将其插入公式编辑器光标所在位置", 44 | "tip3": "公式编辑器中输入任何字符会显示可用变量/函数列表", 45 | "tip4": "公式编辑器中输入 {entrance} 会显示分类可用列表" 46 | }, 47 | "debug": { 48 | "run": "运行", 49 | "result": "结果", 50 | "formula_error": "请先修正公式错误后再运行" 51 | }, 52 | "fun_lib": { 53 | "inner": "内置", 54 | "cate_common": "常用", 55 | "cate_calc": "计算", 56 | "cate_txt": "文本", 57 | "cate_time": "时间", 58 | "cate_api": "接口", 59 | "sum_label": "求和", 60 | "sum_note": "获取一组数值的总和。
用法:SUM(数字1,数字2,...)", 61 | "now_label": "当前时间", 62 | "now_note": "返回当前时间戳", 63 | "concat_label": "合并文本", 64 | "concat_note": "将多个文本合并成一个文本。
用法:concat(文本1,文本2,...)", 65 | "lower_label": "转成小写", 66 | "lower_note": "将一个文本中的所有大写字母转换为小写字母。
用法:lower(文本)", 67 | "upper_label": "转成大写", 68 | "upper_note": "将一个文本中的所有小写字母转换为大写字母。
用法:upper(文本)", 69 | "httpGet_label": "HTTP Get请求", 70 | "httpGet_note": "发起HTTP Get请求,返回Json格式。
用法:httpGet(https://httpbin.org/get)", 71 | "httpGet_input1": "请求地址" 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@idealworld/formula-editor", 3 | "type": "module", 4 | "version": "0.6.2", 5 | "description": "A relatively complete formula editor", 6 | "author": { 7 | "name": "gudaoxuri", 8 | "email": "i@sunisle.org", 9 | "url": "https://idealworld.group/" 10 | }, 11 | "license": "MIT", 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/ideal-world/formula-editor.git" 15 | }, 16 | "keywords": [ 17 | "formula", 18 | "editor", 19 | "code" 20 | ], 21 | "exports": { 22 | ".": { 23 | "types": "./dist/types/index.d.ts", 24 | "import": "./dist/formula-editor.es.js", 25 | "require": "./dist/formula-editor.umd.js" 26 | }, 27 | "./dist/style.css": "./dist/style.css" 28 | }, 29 | "module": "./dist/formula-editor.es.js", 30 | "types": "./dist/types/index.d.ts", 31 | "files": [ 32 | "dist" 33 | ], 34 | "scripts": { 35 | "build": "rimraf dist && vue-tsc && vite build", 36 | "docs:dev": "vitepress dev docs --host 0.0.0.0", 37 | "docs:build": "npm run publish-typedoc && vitepress build docs", 38 | "docs:serve": "vitepress serve docs", 39 | "test": "vitest --run", 40 | "lint": "eslint .", 41 | "lint:fix": "eslint . --fix", 42 | "coverage": "vitest run --coverage", 43 | "publish-typedoc": "typedoc --options typedoc.json" 44 | }, 45 | "peerDependencies": { 46 | "vue": "^3.4.21" 47 | }, 48 | "dependencies": { 49 | "@codemirror/autocomplete": "^6.15.0", 50 | "@codemirror/commands": "^6.3.3", 51 | "@codemirror/lang-javascript": "^6.2.2", 52 | "@codemirror/language": "^6.10.1", 53 | "@codemirror/lint": "^6.5.0", 54 | "@codemirror/search": "^6.5.6", 55 | "@codemirror/state": "^6.4.1", 56 | "@codemirror/view": "^6.26.0", 57 | "@lezer/common": "^1.2.1", 58 | "codemirror": "=6.0.1", 59 | "eslint-linter-browserify": "^8.57.0", 60 | "octicons-css": "^19.8.0", 61 | "vue-codemirror6": "^1.2.5", 62 | "vue-i18n": "^9.10.2" 63 | }, 64 | "devDependencies": { 65 | "@antfu/eslint-config": "^2.8.3", 66 | "@types/node": "^20.11.29", 67 | "@vitejs/plugin-vue": "^5.0.4", 68 | "@vitest/ui": "^1.4.0", 69 | "autoprefixer": "^10.4.18", 70 | "daisyui": "^4.7.3", 71 | "eslint": "^8.57.0", 72 | "happy-dom": "^14.0.0", 73 | "postcss": "^8.4.36", 74 | "postcss-import": "^16.0.1", 75 | "postcss-nesting": "^12.1.0", 76 | "rimraf": "^5.0.5", 77 | "tailwindcss": "^3.4.1", 78 | "typedoc": "^0.25.12", 79 | "typedoc-plugin-markdown": "4.0.0-next.53", 80 | "typedoc-vitepress-theme": "1.0.0-next.9", 81 | "typescript": "^5.4.2", 82 | "unplugin-vue-components": "^0.26.0", 83 | "vite": "^5.1.6", 84 | "vitepress": "1.0.0-rc.45", 85 | "vitest": "^1.4.0", 86 | "vue-tsc": "^2.0.6" 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /example/src/App.vue: -------------------------------------------------------------------------------- 1 | 90 | 91 | 103 | -------------------------------------------------------------------------------- /src/processes/funcLib.ts: -------------------------------------------------------------------------------- 1 | import locales from '../locales' 2 | import { VarKind } from './interface' 3 | 4 | const { t } = locales.global 5 | 6 | /** 7 | * Default function library 8 | */ 9 | export const DEFAULT_FUN_LIB 10 | = { 11 | name: 'fun', 12 | label: t('fun_lib.inner'), 13 | isVar: false, 14 | showLabel: false, 15 | color: '#d9ecff', 16 | items: [ 17 | { 18 | name: 'sum', 19 | label: t('fun_lib.sum_label'), 20 | note: t('fun_lib.sum_note'), 21 | input: [ 22 | { 23 | kind: VarKind.NUMBER, 24 | }, 25 | ], 26 | isVarLen: true, 27 | isAsync: false, 28 | output: { 29 | kind: VarKind.NUMBER, 30 | }, 31 | body: `return Array.from(arguments).reduce((a, b) => a + b)`, 32 | cates: [t('fun_lib.cate_common'), t('fun_lib.cate_calc')], 33 | }, 34 | { 35 | name: 'now', 36 | label: t('fun_lib.now_label'), 37 | note: t('fun_lib.now_note'), 38 | input: [], 39 | isVarLen: false, 40 | isAsync: false, 41 | output: { 42 | kind: VarKind.NUMBER, 43 | }, 44 | body: `return Date.now()`, 45 | cates: [t('fun_lib.cate_common'), t('fun_lib.cate_time')], 46 | }, 47 | { 48 | name: 'concat', 49 | label: t('fun_lib.concat_label'), 50 | note: t('fun_lib.concat_note'), 51 | input: [ 52 | { 53 | kind: VarKind.ANY, 54 | }, 55 | ], 56 | isVarLen: true, 57 | isAsync: false, 58 | output: { 59 | kind: VarKind.STRING, 60 | }, 61 | body: `return Array.from(arguments).join('')`, 62 | cates: [t('fun_lib.cate_common'), t('fun_lib.cate_txt')], 63 | }, 64 | { 65 | name: 'lower', 66 | label: t('fun_lib.lower_label'), 67 | note: t('fun_lib.lower_note'), 68 | input: [ 69 | { 70 | kind: VarKind.STRING, 71 | }, 72 | ], 73 | isVarLen: false, 74 | isAsync: false, 75 | output: { 76 | kind: VarKind.STRING, 77 | }, 78 | body: `return arguments[0].toLowerCase()`, 79 | cates: [t('fun_lib.cate_txt')], 80 | }, 81 | { 82 | name: 'upper', 83 | label: t('fun_lib.upper_label'), 84 | note: t('fun_lib.upper_note'), 85 | input: [ 86 | { 87 | kind: VarKind.STRING, 88 | }, 89 | ], 90 | isVarLen: false, 91 | isAsync: false, 92 | output: { 93 | kind: VarKind.STRING, 94 | }, 95 | body: `return arguments[0].toUpperCase()`, 96 | cates: [t('fun_lib.cate_txt')], 97 | }, 98 | { 99 | name: 'httpGet', 100 | label: t('fun_lib.httpGet_label'), 101 | note: t('fun_lib.httpGet_note'), 102 | input: [ 103 | { 104 | label: t('fun_lib.httpGet_input1'), 105 | kind: VarKind.STRING, 106 | }, 107 | ], 108 | isVarLen: false, 109 | isAsync: true, 110 | output: { 111 | kind: VarKind.ANY, 112 | }, 113 | body: `return await (await fetch(arguments[0])).json()`, 114 | cates: [t('fun_lib.cate_api')], 115 | }, 116 | ], 117 | } 118 | -------------------------------------------------------------------------------- /docs/ExecutorDemo.vue: -------------------------------------------------------------------------------- 1 | 71 | 72 |