03-创建编辑器
03-创建编辑器
创建编辑器
本指南展示了如何使用 Unified 创建一个交互式在线编辑器,有时也被称为 “playground 游乐场” 或 “dingus 小工具” 。在这个编辑器中,我们将通过“语法高亮”来可视化文本的语法属性。它使用 React 构建,并在浏览器中运行。
在这个例子中,我们将创建一个可视化句子长度的应用程序。该应用程序基于 Gary Provost 提供的技巧,其可视化效果则源自 一条推文 by @gregoryciotti。
您也可以在线查看此项目的更多功能。
遇到难题?有其他指南的想法?请参阅
support.md。
内容
实例 (Case)
在开始之前,我们先来概述一下我们想要实现的功能。我们希望根据文本的字数高亮显示句子。用户应该能够修改文本,并且高亮显示功能应该能够实时生效。
我们将使用 esbuild 作为打包工具,将 JavaScript 代码编译成可在生产环境浏览器中运行的代码。我们将使用 xo 和 prettier 来检查和格式化代码。您也可以根据自己的喜好替换这些工具。
项目结构
我们先来概述一下项目结构:
demo/
├─ bundle.mjs
├─ index.css
├─ index.html
├─ index.jsx
└─ package.json...其中 demo/ 是我们的文件夹,bundle.mjs 是编译 index.jsx 生成的 JavaScript
现在将 index.jsx、index.html 保留为空,然后使用以下内容填入 package.json :
{
"devDependencies": {
"esbuild": "^0.23.0",
"prettier": "^3.0.0",
"xo": "^0.59.0"
},
"name": "demo",
"prettier": {
"bracketSpacing": false,
"semi": false,
"singleQuote": true,
"tabWidth": 2,
"trailingComma": "none",
"useTabs": false
},
"private": true,
"scripts": {
"build": "esbuild index.jsx --bundle --format=esm --jsx=automatic --minify --outfile=bundle.mjs --target=es2020",
"format": "prettier . --log-level warn --write && xo --fix",
"test": "npm run build && npm run format"
},
"type": "module",
"xo": {
"envs": [
"browser"
],
"ignore": [
"bundle.mjs"
],
"prettier": true
}
}
private: true意味着你不会意外地将你的包发布到 npm。
上述软件包会安装 xo、prettier 和 esbuild。运行 npm install 和 npm test 后,您也会看到bundle.mjs 出现。
另外,添加 .prettierignore 文件以避免格式化我们的构建:
bundle.mjs请填写index.html以下内容:
<!doctype html>
<meta charset="utf8" />
<title>demo</title>
<link rel="stylesheet" href="index.css" />
<div id="root"></div>
<script type="module" src="bundle.mjs"></script>这链接了 index.css 和 bundle.mjs,并添加一个元素(#root),我们稍后会将编辑器添加到该元素中。
您知道 <html>、<head>、<body> 都是可选的吗?在这个例子中,我们将保持HTML 代码的简洁性,但如果您需要,可以随意添加它们。
设置 JavaScript
好了!现在,我们来设置 JavaScript。首先,将以下代码添加到index.jsx:
/// <reference lib="dom" />
/* eslint-env browser */
import ReactDom from 'react-dom/client'
import React from 'react'
const main = document.querySelector('#root')
if (!main) throw new Error('No root element found')
const root = ReactDom.createRoot(main)
const sample = 'The initial text.'
root.render(React.createElement(Playground))
function Playground() {
const [text, setText] = React.useState(sample)
return (
<div className="editor">
<div className="draw">
{/* Trailing whitespace in a `textarea` is shown, but not in a `div`
with `white-space: pre-wrap`.
Add a `br` to make the last newline explicit. */}
{/\n[ \t]*$/.test(text) ? <br /> : undefined}
</div>
<textarea
className="write"
onChange={(event) => setText(event.target.value)}
rows={text.split('\n').length + 1}
spellCheck="true"
value={text}
/>
</div>
)
}别忘了
npm install @types/react-dom @types/react react-dom react
我能想象,这进展得有点快。
在 Playground 代码中,我们创建了两个元素:一个 div (.draw) 用于绘制语法高亮。一个 textarea (.write) 用于供用户编辑的。两者都被包裹在一个父级 <div> (.editor) 元素中。当用户更改文本区域时,将使用新值调用 setText,这反过来又会导致 text 元素发生变化。
我们将文本区域和绘图区域的样式设置得完全相同,并将文本置于绘图区域上方,样式如下:
html {
font-size: 16px;
line-height: 1.5;
}
.editor {
position: relative;
max-width: 37em;
margin: auto;
overflow: hidden;
}
textarea,
.draw {
margin: 0;
padding: 0;
width: 100%;
border: none;
outline: none;
resize: none;
overflow: hidden;
/* Can’t use a nice font: kerning renders differently in textareas. */
font-family: monospace;
line-height: inherit;
font-size: inherit;
background: transparent;
white-space: pre-wrap;
word-wrap: break-word;
font-size: inherit;
line-height: inherit;
}
textarea {
color: inherit;
position: absolute;
top: 0;
}
.draw {
min-height: 100vh;
}这段代码相当长:主要是为了强制文本区域和绘图区域使用相同的样式。
自然语言句法树
现在,我们来设置自然语言语法树解析。我们将使用retext 解析器 parse-english 来完成这项工作。它是一个适用于 retext 生态系统的解析器,可以生成 nlcst 文件。我们不需要插件,而且我们是在浏览器中使用,浏览器对文件大小的要求更高,所以我们可以直接使用相关的工具。
修改 index.jsx 如下:
--- a/index.jsx
+++ b/index.jsx
@@ -1,5 +1,6 @@
/// <reference lib="dom" />
/* eslint-env browser */
+import {ParseEnglish} from 'parse-english'
import ReactDom from 'react-dom/client'
import React from 'react'
@@ -8,11 +9,13 @@ if (!main) throw new Error('No root element found')
const root = ReactDom.createRoot(main)
const sample = 'The initial text.'
+const parser = new ParseEnglish()
root.render(React.createElement(Playground))
function Playground() {
const [text, setText] = React.useState(sample)
+ const tree = parser.parse(text)
return (
<div className="editor">别忘了
npm install parse-english
太好了,现在我们掌握了很多关于文本的信息。不过它目前还没有任何实际作用。让我们来增加一些实用功能吧。
虚拟DOM
我们的下一个任务是将自然语言语法树转换为虚拟DOM。修改 index.jsx 如下:
--- a/index.jsx
+++ b/index.jsx
@@ -1,5 +1,9 @@
/// <reference lib="dom" />
/* eslint-env browser */
+/**
+ * @import {Nodes, Parents} from 'nlcst'
+ */
+
import {ParseEnglish} from 'parse-english'
import ReactDom from 'react-dom/client'
import React from 'react'
@@ -20,6 +24,7 @@ function Playground() {
return (
<div className="editor">
<div className="draw">
+ {one(tree)}
{/* Trailing whitespace in a `textarea` is shown, but not in a `div`
with `white-space: pre-wrap`.
Add a `br` to make the last newline explicit. */}
@@ -35,3 +40,39 @@ function Playground() {
</div>
)
}
+
+/**
+ * @param {Parents} node
+ * @returns {Array<React.JSX.Element | string>}
+ */
+function all(node) {
+ /** @type {Array<React.JSX.Element | string>} */
+ const results = []
+ let index = -1
+
+ while (++index < node.children.length) {
+ const result = one(node.children[index])
+
+ if (Array.isArray(result)) {
+ results.push(...result)
+ } else {
+ results.push(result)
+ }
+ }
+
+ return results
+}
+
+/**
+ * @param {Nodes} node
+ * @returns {Array<React.JSX.Element | string> | React.JSX.Element | string}
+ */
+function one(node) {
+ const result = 'value' in node ? node.value : all(node)
+
+ if (node.type === 'SentenceNode') {
+ return <span>{result}</span>
+ }
+
+ return result
+}别忘了
npm install @types/nlcst
all 在给定的节点中搜索所有子节点 node,其中 one 返回节点的 “文本内容 (text content)”,或者再次搜索其 all 子节点的结果。
现在,当你运行 npm test 并在浏览器打开 index.html 后,你会看到绘图区域已经包含了我们的文本。虽然还没有着色,但样式隐藏的 <span> 元素正在包装句子。
强调
现在,我们来添加颜色。想这样更新 index.jsx :
--- a/index.jsx
+++ b/index.jsx
@@ -7,6 +7,7 @@
import {ParseEnglish} from 'parse-english'
import ReactDom from 'react-dom/client'
import React from 'react'
+import {visit} from 'unist-util-visit'
const main = document.querySelector('#root')
if (!main) throw new Error('No root element found')
@@ -14,6 +15,7 @@ const root = ReactDom.createRoot(main)
const sample = 'The initial text.'
const parser = new ParseEnglish()
+const hues = [60, 60, 60, 300, 300, 0, 0, 120, 120, 120, 120, 120, 120, 180]
root.render(React.createElement(Playground))
@@ -71,7 +73,23 @@ function one(node) {
const result = 'value' in node ? node.value : all(node)
if (node.type === 'SentenceNode') {
- return <span>{result}</span>
+ let words = 0
+
+ visit(node, 'WordNode', function () {
+ words++
+ })
+
+ const hue = words < hues.length ? hues[words] : hues.at(-1)
+
+ return (
+ <span
+ style={{
+ backgroundColor: 'hsl(' + [hue, '93%', '70%', 0.5].join(', ') + ')'
+ }}
+ >
+ {result}
+ </span>
+ )
}
return result别忘了
npm install unist-util-visit
这段代码导入 unist-util-visit 并定义了一些色调。我们试图通过以下方式重现该 视觉效果 by @gregoryciotti。我从那张图片中提取了这些色调。当然,您也可以使用任何您喜欢的色调!
然后,我们统计每个句子中的单词数量。接着,我们根据单词数量,使用一种HSL颜色作为每个句子的背景色。
再次运行 npm test 并在浏览器中查看 index.html。如果一切正常,您应该会看到每句话都以红色高亮显示。
更多练习
💃 现在你应该能在浏览器中看到紫色的 The initial text 部分了!如果你添加更多句子,每个句子都会根据字数显示不同的颜色。
它的样式还可以做得更好,但除此之外,它还是一个相当不错的小演示。
如果你还没看过,请查看 学习版块 中的其他文章!