=====================
== 端阳的主页 ==
=====================

免费体验!开启吉卜力风格转换之旅

AI工具

免费体验!开启吉卜力风格转换之旅

0. 平台介绍

Ghiblio Art是一款专业的吉卜力风格AI图像生成平台,利用强大的ChatGPT 4o模型,将文字和照片转化为充满魔力的吉卜力插画。无论是柔和的水彩渐变、奇幻的建筑还是空灵的光效,Ghiblio Art都能创造出真实的吉卜力风格艺术作品。体验地址:https://ghiblio.art/?utm_source=7QB85D9QwaJe

1. 功能特性

  • 文生图与图生图:通过输入文字描述或上传图片生成吉卜力风格的插画。
  • 多种风格支持:除了吉卜力风格,还支持多种动漫风格。
  • 批量生图:支持多图生多图功能,满足更大规模的创作需求。

2.风格介绍

史努比风格

  • 特点:史努比风格以简单的线条和鲜明的色彩著称,通常展现轻松幽默的场景,非常适合儿童和家庭主题。
  • 示例图
    效果

吉卜力风格

  • 特点:吉卜力风格以细腻的笔触和丰富的色彩著称,常常展现自然与人类和谐共处的场景,充满梦幻与温暖。
  • 示例图
    效果

Q版风格

  • 特点:Q版风格以夸张的比例和可爱的形象为特点,通常用于表现卡通化和萌系角色,非常适合轻松可爱的主题。
  • 示例图
    效果

3. 使用体验

无需具备绘画技能,只需输入文字描述或上传图片,即可生成精美的多种风格图像。无论是艺术新手还是专业设计师,都能轻松创作。

4. 试用账号

充了50,发现用不完,有需要的自取。

账号:duanyangchn@qq.com

密码:ghiblioart

想注册自己账号的,也可用下面的邀请链接:https://ghiblio.art/?utm_source=7QB85D9QwaJe

探索Yourware.so:简化网页部署的创新平台

AI工具

探索Yourware.so:简化网页部署的创新平台

0.前言

正吃瓜https://8zo2eorjmx.app.yourware.so,意外发现这个平台有点意思,可以一键部署网页。

想想以前,部署个网页也是费老劲了。云服务器得搞一个,这最最基本的。就按最简单的来,得在服务器上装个Nginx,然后丢个静态页到Web服务器根目录。

然后搞个个性域名,添加解析这都基本操作。服务器如果是国内的话,还得ICP备案和公安备案,用境外服务器倒可以省略这一步骤。

后来有了GitHub Pages,创建对应的仓库和指定名称的分支,就能直接访问到网页。配合GitHub Actions还能实现自动部署。但GitHub Pages仅支持静态网站。

再然后,又有Vercel这种的托管服务,前端后端都能托管。关联仓库后,当代码有更新时,Vercel直接自动拉取代码并部署。

现在,如果想快速呈现AI生成的代码效果。可以试试Yourware.so。

1.什么是Yourware.so

Yourware.so是一个专门用于上传和托管HTML或TSX文件的平台。它允许用户把人工智能生成的HTML或TSX代码上传到该平台,并将其转换为托管在云端的网页。用户只需上传、粘贴或拖放文件,就可以获得一个实时网站,无需任何配置和复杂的部署过程。

功能亮点

  1. 一键部署:访问Yourware.so,上传本地文件或粘贴代码后点击Deploy,等待片刻。

  2. 社区资源:Yourware.so提供了丰富的社区资源和便捷的管理功能,聚合了全球创作者的AI生成案例,激发灵感并推动AI创意生态。

  3. 支持AI创意:平台特别适合AI驱动的创意发展,用户可以通过AI生成的代码快速实现他们的想法。

2.总结

Yourware.so是一个强大的工具,旨在简化网页部署过程。它的出现为开发者提供了一个高效的平台来展示创意和项目。总之,快就完事了。

下来找找看有没有接口,也许可以搞个脚本直接上传本地复杂单页应用。

白嫖is-a.dev个性域名

白嫖is-a.dev个性域名

0.前言

日常冲浪,发现可以白嫖两个个性域名。都是GitHub上的项目,is-a-dev和is-a-good-dev,顾名思义,一个可以注册.is-a.dev域名,另一个可以注册.is-a-good.dev域名。

1.注册步骤

首先,需要fork对应的GitHub仓库。

然后,在domains文件夹中创建一个名为your-domain-name.json的新文件,以注册your-domain-name.is-a.dev或your-domain-name.is-a-good.dev。

提交一个Pull request以供审核。审核通过后即可生效。

听起来很简单,操作也不复杂。但是,亲测is-a-dev的PR审核比较严苛。

两个项目下,我都是添加的CNAME解析,对应的json文件如下

{
  "owner": {
    "username": "Mrliduanyang",
    "email": "duanyangchn@gmail.com"
  },
  "record": {
    "CNAME": "duanyang.cool"
  },
  "proxied": false
}
{
  "owner": {
    "username": "Mrliduanyang",
    "email": "duanyangchn@gmail.com"
  },
  "target": {
    "CNAME": {
      "name": "duanyang",
      "value": "duanyang.cool"
    }
  },
  "proxied": false
}

再说is-a-dev的PR审核。

  • 一开始网页上放了个Hello World,不行,网页不完整,让修改。

  • 又让AI随便生成一个页面放上去,还不行,不完整,并且网页内容和开发无关,继续修改。

  • 没办法,找了之前写的几篇博客文章,用Hugo生成静态页,放上去,审核通过。

相比之下,is-a-good-dev的审核就宽松多了。

原因可能有两点吧。一是is-a-good-dev没有is-a-dev知名,从PR数量上也能反映,is-a-good-dev只有700+的PR,而is-a-dev有20000+;二可能和审核人员有关,他觉得行就给通过。

PR合并后,域名很快就生效,几乎没等多长时间。

2.最后

一直白嫖一直爽,希望项目不要倒了。感恩!

浏览器运行Deepseek本地模型

DeepSeek 浏览器

浏览器运行DeepSeek本地模型

0.前言

DeepSeek很火,毋庸置疑的火。一火吧,用的人就多,官网那个就卡,不然也不会有那么多第三方部署。也有很多本地部署的方案,比如基于Ollama部署。

但如果只是想尝尝鲜、玩一玩,不想折腾那么复杂的运行环境,那可以看看本文,直接在浏览器里运行DeepSeek。

1.浏览器中运行DeepSeek

为了在浏览器中高效运行DeepSeek,Hugging Face提供了transformers.js库,该库利用WebGPU技术来加速模型的推理过程。

同时,Hugging Face也提供了一系列适用于transformers.js的ONNX格式模型权重文件。

有运行时也有模型,开搞!

有NodeJS环境

推荐使用Node18,减少踩坑。

clone代码、run一把梭。

git clone https://github.com/huggingface/transformers.js-examples.git

cd transformers.js-examples/deepseek-r1-webgpu

npm i

npm run dev

在浏览器中输入http://localhost:5173打开页面,按操作提示加载模型后,即可和DeepSeek模型对话。

没有NodeJS环境

一点小小的限制:因为运行大模型是一个计算密集型的任务,为了不阻塞主线程,所以需要在worker线程中运行DeepSeek推理。

这就带来个新的问题,如果在本地双击html文件打开时,Web Worker会因为跨域限制而无法加载脚本。浏览器要求Web Worker的脚本必须通过 HTTP/HTTPS 协议加载,而不是直接从本地文件系统加载。

所以仍然需要一个HTTP服务器来提供文件服务。下载文件(https://github.com/Mrliduanyang/zhouzhou/blob/main/杂七杂八/dist.tar.gz),解压,然后在index.html所在路径下执行

# Mac和Linux系统都有基本的Python环境,Windows系统可自行下载安装Python环境

python3 -m http.server 5173

启动HTTP服务器。

在浏览器中输入http://localhost:5173打开页面,按操作提示加载模型后,即可和DeepSeek模型对话。

测试

在一台2019年,2.6 GHz 六核Intel Core i7的MacBook Pro上,速度可以达到7token/s。

在一台2021年,Apple M1 Pro的MacBook Pro上,速度可以达到34token/s。

简单问了一个数学题,效果如下:

效果

参考资料

https://github.com/huggingface/transformers.js-examples/tree/main/deepseek-r1-webgpu

手搓Mini Redis(Rust版)系列一

Redis Rust

手搓mini-redis(Rust版)系列一

0.前言

距上一篇分享LZW压缩算法已经过去了几个月了,是时候开新坑了。

最近花了点时间入门了一点Rust,在网上寻找学习资源的时候,发现了这个Tokio官方出的mini-redis,这是一个使用Rust实现的Redis服务端和客户端。但只实现了核心的GET、SET、PUBLISH、SUBSCRIBE功能。简单研究了一下,确实比较适合对初期学习到的各种Rust理念和操作进行一个巩固。

本期先分享mini-redis的数据结构部分。

1.mini-redis

mini-redis数据结构

一切从简,按照最简单最基本的方式来实现Redis。先来想一下,怎么存Redis中的键值对?最简单的方式就是用HashMap,所以先定义一个Redis数据状态类型:

struct State {
    entries: HashMap<String, Bytes>,
}

注意到Redis中的key是有过期时间的,所以我们需要修改下entries的值的类型,增加一个过期时间字段:

struct Entry {
    data: Bytes,
    expires_at: Option<Instant>,
}

struct State {
    entries: HashMap<String, Entry>,
}

这样看起来,存储键值对的部分就比较完善了。

接下来解决过期时间的问题,希望能有一种数据结构能直接取到最早过期的key,同时要考虑到多个key在同一时刻过期的问题,所以我们需要用BTreeSet加元组来存储过期时间,如下所示:

struct State {
    entries: HashMap<String, Entry>,
    expirations: BTreeSet<(Instant, String)>,
}

为什么是BTreeSet,首先BTree部分没啥争议,因为我们需要其中的元素有序,这样才能最快找到最早的过期时间。而Set是为了确保同一个过期时间+key的组合只有一个,想一下,存多个相同的过期时间+key组合也没什么意义。

这样一来,Redis数据存储部分的数据结构就设计完成了。

Redis中还有一项比较重要的功能:发布-订阅,我们来实现它。还得用HashMap,key是订阅的数据键,value则是一个消息发送者,当有一个key需要发送广播消息给订阅该key的订阅者时,直接取出发送者调用发送即可。所以我们进一步扩展State的结构:

struct State {
    entries: HashMap<String, Entry>,
    expirations: BTreeSet<(Instant, String)>,
    pub_sub: HashMap<String, broadcast::Sender<Bytes>>,
}

既然是用来存取数据的,我们要保证最起码的数据一致性,同一个数据,同一时刻,不能有多个写者。用最基本的互斥锁来实现就好,读写的时候都需要先获取锁,之后再进行读写,而没有获取到锁的则等待。我们在State结构上包一层,于是有了:

struct Shared {
    state: Mutex<State>,
}

了解Rust的应该清楚,Rust中通过所有权系统来保证内存安全,但所有权同时也限制了数据共享。而我们的Redis不可能一次只服务一个客户端,那要如何实现多线程之间的数据共享呢?也很简单,Rust提供了Arc(原子引用计数),这个结构通过Clone可以为共享的数据创建一个引用指针,同时增加引用计数器。所以我们在Shared结构上再包一层Arc,就有了:

struct Db {
    shared: Arc<Shared>,
}

数据结构之上的Redis操作

有了基础的数据结构定义后,我们来实现所需要的Redis能力。首先构造函数得有:

Read more...

手搓Lzw压缩算法

压缩算法 JavaScript

手搓LZW压缩算法

0.前言

在写完上一篇《手搓编辑器》后,一直在想手搓系列能写啥,考虑过手搓操作系统、手搓数据库,但发现有点不自量力,根本不是一两篇文章能讲明白的,于是转去寻找有没有一些短小精悍、又带点难度的东西。

在偶然的一天,看了眼前端项目线上构建产物,忽然想到代码压缩时候用的是什么压缩算法呢?发现手搓压缩算法这个选题有点搞头。

那就从经典的LZW压缩算法开始。

1.LZW压缩算法

LZW压缩算法的基本原理:提取源文件中的不同字符,基于这些字符创建一个字典,然后用字典中的字符的索引来替代源文件中的相应字符,从而减少原始数据大小。

看到这里,第一感觉那岂不是得先处理一遍源文件以获得这个所谓的字典。但其实并不是,这也是LZW算法第一个精妙的点,算法运行的时候,会根据已读取字符来逐步构建字典,这样一来,当后面再读取到相同的字符串时,即可以用字典中的索引来代替。这也使得压缩后的结果是自解释的,即字典是不会被写进压缩文件的。

LZW压缩算法是的解压过程其实和压缩过程很像,也是读取压缩文件,构建字典,还原文件。但由于压缩文件不包含字典,所以在解压过程中会遇到一种特殊情况:如果一个索引第一次出现,该如何确定它对应的字符串? 这个问题的解法是LZW压缩算法中第二个精妙的点,解决方案很简单,但理解这个解法有点绕,需要反复揣摩。此处暂且不表,后面来攻克它。

压缩过程

不引入过多概念,给定一个待压缩字符串ababcababac,用自然语言来描述一下LZW压缩算法的压缩过程。

先尝试理解一下字典是如何构建的。

生成一个初始态字典,为待压缩字符串中所有的单字符添加索引:

const dictionary = {
    a: 0,
    b: 1,
    c: 2,
}
  1. 读取到第1个字符a
    • 发现在字典中,继续;
  2. 读取到第2个字符b
    • 和上一步的字符a构成字符串ab;
    • 发现ab不在字典中,添加映射ab: 3
  3. 读取到第3个字符a
    • 和上一步的字符b构成字符串ba;
    • 发现ba不在字典中,添加映射ba: 4
  4. 读取到第4个字符b
    • 和上一步的字符a构成字符串ab;
    • 发现ab在字典中,继续;
  5. 读取到第5个字符c
    • 和上一步的字符串ab构成字符串abc;
    • 发现abc不在字典中,添加映射abc: 5
  6. 读取到第6个字符a
    • 和上一步的字符c构成字符串ca;
    • 发现ca不在字典中,添加映射ca: 6
  7. 读取到第7个字符b
    • 和上一步的字符a构成字符串ab;
    • 发现ab在字典中,继续;
  8. 读取到第8个字符a
    • 和上一步的字符串ab构成字符串aba;
    • 发现aba不在字典中,添加映射aba: 7
  9. 读取到第9个字符b
    • 和上一步的字符a构成字符串ab;
    • 发现ab在字典中,继续;
  10. 读取到第10个字符a
    • 和上一步的字符串ab构成字符串aba;
    • 发现aba在字典中,继续
  11. 读取到第11个字符c
    • 和上一步的字符串aba构成字符串abac
    • 发现abac不在字典中,添加映射abac: 8
  12. 字典构建结束

先来回顾后面的压缩是如何利用前面添加的映射的:

  • 映射ab: 3是在第2步添加的,然后在第4步和第7步中就发现ab已在字典中存在,所以可以进行压缩;
  • 映射aba: 7是在第8步添加的,然后在第10步中就发现aba已在字典中存在,所以可以进行压缩。

再关注下压缩过程中提到“继续”的地方,会发现一个特别之处,那就是“继续”的时候,表明可以尝试在前一个字符串的基础上继续添加一个字符,从而组成更长的字符串。当无法构成更长已知字符串时,则从当前字符重新开始增长,像滑动窗口一样。

再来看一个值得思考的地方,注意到ababac子串,给之间加上空格以示区分ab aba c,并标记为第一段、第二段、第三段。对照压缩过程的第8步,添加映射aba: 7,这里的字符串aba其实是第一段的ab和第二段的第一个字符a组成的。添加到字典后,紧跟着第9步、第10步就继续读取字符b、字符a,组成了字符串aba并发现存在于字典中。

Read more...

手搓编译器

编译器 JavaScript

手搓编译器

本文手搓了一个JavaScript到JSON的编译器。先别奇怪,这不是一个JSON.stringify就能实现的吗?怎么还搞上编译器了呢。原因有两点:

  1. JSON语法足够简单,确保手搓出来的编译器代码不会太复杂;
  2. JavaScript源码和JSON有些差异,使得代码生成阶段有事可做。

本文代码实现基于https://github.com/jamiebuilds/the-super-tiny-compiler,加入了自己的理解,并且实现的功能相比于原文将类似于LISP的函数调用编译成类似于C的函数调用而言要更加复杂,能更好的帮助读者理解编译器的细节。

原文的翻译版地址在https://github.com/YongzeYao/the-super-tiny-compiler-CN,感兴趣的读者可以先看这篇,递归算法基础扎实的话,20分钟可以掌握。本文创作过程中有参考翻译版中的内容。

对递归思想理解有困难的话,可以先看看往期分享:

0.前言

想当年,我在学编译原理这门课的时候,就感觉那些名词、那些话是人能看懂的?什么短语、句柄、规约、LR分析,这都是啥。

随着计算机知识的增加,又知道大部分编译器的工作可以被分解为三个主要阶段:

  • 解析(Parsing)
  • 转化(Transformation)
  • 代码生成(Code Generation)。

先说解析,一般被分为两个部分:词法分析和语法分析。词法分析将源代码分解成一个个词素,它可以描述数字,标识符,标点符号,运算符等。

语法分析接收词素并将它们组合成一个描述源代码各部分之间关系的中间表达形式:抽象语法树。抽象语法树是一个深度嵌套的对象,这个对象以一种既能够简单地操作又提供很多关于源代码信息的形式来展现代码。

转换阶段过程接收解析生成的抽象语法树并对它做出改动。转换阶段可以改变抽象语法树使代码保持在同一个语言或者编译成另外一门语言。

编译器的最后步骤是代码生成。有时候编译器在这个步骤也会执行转换阶段的一些行为,但是大体而言代码生成阶段的工作就是基于转换步骤产生的抽象语法树生成目标代码。

想想整个编译的过程也很符合直觉,假如现在想描述一个小狗,那以小狗作为树的根节点,叶子节点就可以是头、四肢、躯干等,头又细分到五官,沿着各个节点继续向下生长,最终得到一个树形描述。

假如想得到一个小金毛的描述,那就沿着树,开始搜索,当搜索到躯干时,给表示躯干的节点添加一个描述“金色毛发”,这样就得到一个转化后的树形描述。最后,再按照这个描述将“小狗”组合起来就可以知道小金毛到底是个什么样子。

抛开教材上艰深晦涩的编译原理,从代码实现上来手搓一个编译器,会对编译这个过程有更加深刻的理解。

1.进入正题

我们的目标是实现一个从JavaScript到JSON的代码转换器,主要就是给JavaScript中的标识符加上双引号。

JavaScript源代码

const jsSourceCode = `{
  aaa: 1,
  bbb: [
    2,
    3,
    4,
    {
      cc: {
        d: "My Compiler",
      },
    },
  ],
  eeeee: {
    ffffff: {
      gg: 5,
      hhhh: 6,
    },
  },
}`;

词法分析

// 我们的目的是把JS转为对应JSON,所以只需处理源代码中的六个构造字符{}[]:,
// 以及字符串、数字、标识符(其实也是字符串,出现在:前的字符串)
// 三个字面值(false、null、true)我们先不考虑
function tokenizer(input) {
  // 指针位置
  let current = 0;
  let tokens = [];
  // 扫描字符串,源代码可以看做一个字符序列,从头扫描到尾一遍,因此使用while即可
  while (current < input.length) {
    // 当前字符
    let char = input[current];
    // 识别左大括号
    if (char === "{") {
      tokens.push({
        type: "Punctuator",
        value: "{",
      });
      current++;
      continue;
    }
    // 识别右大括号
    if (char === "}") {
      tokens.push({
        type: "Punctuator",
        value: "}",
      });
      current++;
      continue;
    }

    if (char === "[") {
      tokens.push({
        type: "Punctuator",
        value: "[",
      });
      current++;
      continue;
    }

    if (char === "]") {
      tokens.push({
        type: "Punctuator",
        value: "]",
      });
      current++;
      continue;
    }

    if (char === ":") {
      tokens.push({
        type: "Punctuator",
        value: ":",
      });
      current++;
      continue;
    }

    if (char === ",") {
      tokens.push({
        type: "Punctuator",
        value: ",",
      });
      current++;
      continue;
    }
    // 看懂以上单个符号的识别判断应该毫无压力
    
    // 跳过源代码中无意义的空字符,比如:空格
    let WHITESPACE = /\s/;
    if (WHITESPACE.test(char)) {
      current++;
      continue;
    }
    
    // 识别数字,简单处理,不搞花里胡哨,就最简单的判断数字序列
    // 哪怕它是个007,也认为是个合法的数字
    let NUMBERS = /[0-9]/;
    if (NUMBERS.test(char)) {
      let value = "";
      while (NUMBERS.test(char)) {
        value += char;
        char = input[++current];
      }
      tokens.push({ type: "Numeric", value });
      continue;
    }

    // 识别字符串,简单处理,用""包裹的内容视为字符串
    if (char === '"') {
      // 生成词素时,定义字符串以"开头,因此添加一个"
      let value = '"';
      char = input[++current];
      // 到尾"之前的内容,不管是什么,都是当前词素的组成
      while (char !== '"') {
        value += char;
        char = input[++current];
      }
      // 定义字符串以"结尾,因此添加一个"
      value += '"';
      // 跳过源代码中的"
      // 操作看似有点多余,上一行加一个",这一行又要跳过
      // 但想一下,如果源代码中的字符串是''包裹的,这里的处理就很有必要了
      char = input[++current];

      tokens.push({ type: "String", value });
      continue;
    }

    // 识别标识符,简单处理,不搞花里胡哨,就只有大小写字母,不管_什么的
    let LETTERS = /[a-z]/i;
    if (LETTERS.test(char)) {
      let value = "";
      // 连续的大小写字母序列,就是一个标识符
      while (LETTERS.test(char)) {
        value += char;
        char = input[++current];
      }

      tokens.push({ type: "Identifier", value });
      continue;
    }

    throw new TypeError("I dont know what this character is: " + char);
  }
  return tokens;
}

语法分析

function parser(tokens) {
  // 指针位置,用来建立抽象语法树和词素列表的联系
  // 告诉walk递归函数,当游走到抽象语法树的某个位置时,当前的词素是哪一个
  let current = 0;

  // 想一下人是怎么读代码的,假如读到一个有子结构的地方,是不是要深入到子结构内部,直到把整个子结构读完了,才能返回,继续往下读
  // 这里很显然是一个深度优先的思想,所以我们用递归来实现
  // 用递归来实现的话,只需要想清楚一层要怎么处理,至于涉及到的子结构,交给递归去处理
  function walk() {
    // 当前词素
    let token = tokens[current];

    // 递归结束条件,当词素类型是数字或者字符串的时候,就已经是最小结构了,直接返回对应节点
    // 检测是否是数字
    if (token.type === "Numeric") {
      current++;
      return {
        type: "Literal",
        value: token.value,
      };
    }

    // 检测是否是字符串
    if (token.type === "String") {
      current++;
      return {
        type: "Literal",
        value: token.value,
      };
    }

    // 检测是否是一个数组
      
    // 想想JavaScript里数组的语法,就是一个[,加若干个元素,再加一个]构成
    // 至于中间元素要怎么构成,不用管,交给递归,相信递归算法可以帮你返回正确的数组元素
    if (token.type === "Punctuator" && token.value === "[") {
      // 跳过[,抽象语法树里[没有意义
      // 因为在抽象语法树里,不需要关心数组到底是怎样的开头
      // 你甚至可以在代码生成的时候,用#来包裹数组
      token = tokens[++current];
      // 创建数组的抽象语法树树节点表示
      let node = {
        type: "ArrayExpression",
        elements: [],
      };
      // while循环直到遇到],说明数组结束
      while (!(token.type === "Punctuator" && token.value === "]")) {
        // 跳过,因为在抽象语法树里不需要
        if (token.value === ",") {
          token = tokens[++current];
          continue;
        }
        // 不确定数组元素到底是怎么构成的,直接递归  
        node.elements.push(walk());
        token = tokens[current];
      }
      // 跳过]
      current++;
      // 返回数组类型的节点。
      return node;
    }

    // 检测是否是一个对象
    // 想想对象的语法,就是一个{,加若干个键值对,再加一个}构成
    // 那键值对的语法呢,就是一个key(标识符),加一个:,加一个value,加一个,
    if (token.type === "Punctuator" && token.value === "{") {
      // 跳过{,抽象语法树里{没有意义,原因同上面数组部分
      token = tokens[++current];
      // 创建对象的抽象语法树树节点表示
      let node = {
        type: "ObjectExpression",
        properties: [],
      };

      // while循环直到遇到},说明对象结束
      while (!(token.type === "Punctuator" && token.value === "}")) {
        // 提取key
        const key = token.value;
        // 跳过:
        token = tokens[++current];
        // 提取value
        token = tokens[++current];
        // key和value都有了,可以向对象的properties中添加一个属性
        node.properties.push({
          type: "Property",
          key,
          // 不确定value到底是怎么构成的,直接递归
          value: walk(), 
        });
        // 此处要求属性最后必须有“,”,否则会有解析异常,可以优化。有些JavaScript代码风格喜欢对象的最后一个属性后不加“,”
        // 跳过,
        token = tokens[++current];
      }
      // 跳过}
      current++;
      // 返回这个节点。
      return node;
    }

    // 同样,如果我们没有匹配到以上任何类型,我们抛出一个错误。
    throw new TypeError(token.type);
  }
  // 定义抽象语法树的根节点
  let ast = {
    type: "Program",
    body: [],
  };
  // 开始生成
  while (current < tokens.length) {
    ast.body.push(walk());
  }
  return ast;
}

转换器

这块代码和原文中的实现有一些差异,主要是原文中给原抽象语法树节点增加_context的技巧,在这里不是很适用。

Read more...
1 of 1