前言:对于开发语言,我们大部分都是使用者,有没有辣么一天,我们也成为一个创造者,设计一门新语言给别人使用呢?本文,将介绍如何创建一个门新语言,以及新语言的配套设施。
背景
笔者刚刚啃完编译原理这门课程。又碰巧在写一个问卷系统时遇到问题这样的一个问题(如下图)。
使用外部DSL,将表单很难实现的逻辑表达,改成编码表达。
一、大胆且美好的构想
一个大胆且美好的构想:让产品来写代码吧!
让产品来写代码需要分几步?
答:三步,告诉产品汪用哪个编程语言,然后让产品汪写代码,最后运行。
先看看最终效果
美好的构想拆解
1.1、第一步:选择编程语言
程序员打代码主要用到的编程语言是Java、C++、Javascript等,这类语言统称为:通用编程语言(General Purpose Language,简称GPL),学习周期很长,而且需要有编程基础,让非开发人员使用 GPL 语言来编写代码显然是不现实。而与之相对应 领域特定语言(Domain Specific Language,简称 DSL),顾名思义,专门未来解决单一领域的问题而诞生的一门逻辑语言,最常见的 DSL 包括 HTML & CSS 、 Markdown 、Regex(正则表达)等。
1.2、第二步:如何能让无编程基础的产品汪写代码
我们要开发一个DSL的编译器,最好跟MarkDown编辑器一样好用。最好能有语法高亮、语法提示补齐、完整的示例等等;
1.3、第三步:如何运行代码(可选)
最好不用主动编译,或者能自动编译。最好能和Markdown编译器一样,一边写代码,一边看效果。(不用主动运行,即这步可以忽略)
二、着手实现
2.1、 设计一门新语言
设计一门语言,需要大量工作量,为了省事,笔者直接给出已经设计好的新语言(HJ
)
2.2 开发HJ编辑器
提供一个对无编程经验的人员友好的编辑器。
- 写代码过程: 基于Monaco Editor,来开发编辑器,提供语法高亮和输入建议和补全;
- 效果展示:实时监听代码输入,将
HJ
转成JS
后执行,实时展现页面效果;
2.2.1 编译原理
这里会用到些编译原理的知识,编译原理四个基本步骤:
2.2.2 语法高亮
- 实现原理,基于编译原理第一步的词法分析,使用正则匹配中的字符串,并进行配色。
- 具体代码实现如下:
monaco.languages.register({ id: LangId })
monaco.languages.setMonarchTokensProvider(LangId, {
tokenizer: {
root: [ // token 解析规则
[/#(.*)?/, 'noUseSign'],
[/\s*(Q[0-9]*(S[0-9]*)?(A[0-9]*)?)|END\s*/, 'Question'],
[/\s*(if|then)\s*/, 'IfStatement'],
[/\s*(limit|of)\s*/, 'LimitStatement'],
[/\s*(from|to)\s*/, 'BranchStatement'],
[/\s*(show|hide|replace|title|branch|shuffle)\s*/, 'action'],
[/\s*(> |== |>= |<= |<)\s*/, 'compareSign'],
[/\s*(and|or|not|\(|\))\s*/, 'linkSign'],
[/\s*([0-9]\d*)\s*/, 'Number'],
[/\s*(,)\s*/, 'Gap'],
],
},
keywords: [
'if',
'then',
'show',
'hide',
'replace',
'title',
'branch',
'shuffle',
'in',
'with',
'to',
],
operators: ['>', '==', , '>=', '<=', '<', 'and', 'or', 'not', '(', ')'],
whitespace: [
[/[ \t\r\n]+/, 'white'],
[/#(.*)/, 'comment', '@comment'],
],
})
monaco.editor.defineTheme(LangId, {
base: 'vs',
inherit: true,
rules: [ // 配色
{ token: 'Gap', foreground: '01DFA5' },
{ token: 'Number', foreground: 'FA5858' },
{ token: 'Question', foreground: '0082FF' },
{ token: 'noUseSign', foreground: 'BDBDBD' },
{ token: 'linkSign', foreground: '840095' },
{ token: 'compareSign', foreground: 'AEB404' },
{ token: 'IfStatement', foreground: '840095' },
{ token: 'LimitStatement', foreground: '840095' },
{ token: 'BranchStatement', foreground: '840095' },
{ token: 'action', foreground: '840095' },
],
colors: {
'editorLineNumber.foreground': '#999999',
},
})
2.2.3 输入建议和补齐
效果如下:
使用 monaco.languages.registerCompletionItemProvider
接口来实现
monaco.languages.registerCompletionItemProvider(LangId, {
provideCompletionItems: (model, position) => {
return {
// suggestions
{
label: `[问题] Q1 最常用下列哪个通信产品?`,
kind: monaco.languages.CompletionItemKind.Keyword,
insertText: `Q1`,
},
{
label: `[问题] Q2 你为什么最常用QQ?`,
kind: monaco.languages.CompletionItemKind.Keyword,
insertText: `Q2`,
}
// 更多提示
}
}
})
2.2.4 实时效果展示
将【2.2.1】的编译出来的JavaScript,通过eval
或者new Function
运行即可
预览代码实现:
// 提前装备两个类,类比浏览器的window对象
class Quetion{
show(key){} // 显示
hide(key){} // 隐藏
// ... 还有其他方法
}
class Answer{
fn(key){} // 逻辑比较
// ... 还有其他方法
}
const Q = new Quetion()
const A = new Answer()
// 生成渲染函数
const render = new Function('Q','A',
`Q.hide("Q2", "Q3", "Q4");
if(A.fn("Q1A1")){Q.show("Q2")};
if(A.fn("Q1A2")){Q.show("Q3")};
if(A.fn("Q1A3")){Q.show("Q4")};`
)
// 执行
render(Q,A)
总结
至此,我们创建一门新语言 HJ
,并为围绕该语言开发了一个web版的编辑器。这样,产品就在极低的学习成本下就可以按照我们设定的语法来编写代码了。
【关注作者,一起探索前端的边界】