@abrovink/abmedium 中文文档教程
Abmedium
Abmedium 是一种图形媒体。 与文本不同,它可以包含具有多个父节点的循环和节点。
它专为多语言环境中的分布式结构化编辑而设计。 一个节点可以在不同的层 中具有值。 图层可以放在图层合成中并进行投影。 这样可以从一个文档创建不同的投影。 投影可以包含本地化内容,并模拟功能切换/分支。
层在堆栈中投影时可能会发生冲突。 这称为分歧,是 Abmedium 处理的两种冲突类型之一。 另一种类型是同时性。 它们在同时编辑节点时发生。
Terminology
- Documents is a collection of layers and compositions.
- Layers are sets of nodes.
- All nodes have Labels. They can be numbers or strings.
- Nodes are values with a label.
- Values are sequences, strings, texts, symbols, numbers, refs or nil.
- Projections are created when a composition is projected.
- Layer Composition describes which layers, and in what order, to project.
- Disagreements are created during a projection. They represent a mismatch between an expected and actual value.
- Simultaneities are created when new values are added concurrently to the same node.
Document Structure
让我们将内容 [["a", 1], ["b", 2], ["c", 3]]
放入 Abmedium 文档中。 这是通过将内容解构为节点来完成的。
let fruits = Automerge.from(document<{}>());
fruits = Automerge.change(fruits, (doc) => {
const base = doc.layers.base;
base[0] = node(0, seq([1, 2, 3]), {});
base[1] = node(1, seq([4, 5]), {});
base[2] = node(2, seq([6, 7]), {});
base[3] = node(3, seq([8, 9]), {});
base[4] = node(4, str("apple"), {});
base[5] = node(5, num(1), {});
base[6] = node(6, str("banana"), {});
base[7] = node(7, num(2), {});
base[8] = node(8, str("pear"), {});
base[9] = node(9, num(3), {});
});
这是一种非常麻烦的格式。 不要害怕! 该库旨在在后台使用,而不是像本文档中那样直接操作。
为了确保我们没有丢失我们的内容结构,让我们重建它!
const stringPresenter: NodePresenter<{}, string> = presNodeswitch({
seq: (_, items) => `[${items.join(", ")}]`,
str: (n) => `"${n.value}"`,
_: (n: PresNode<any, string>) => String(n.value),
});
let out = pres(proj(fruits), stringPresenter);
console.log("1.", out);
// ⇒ 1. [(["apple", 1], ["banana", 2], ["pear", 3])];
这里发生了很多事情。 首先,让我们谈谈房间里的大象。 为什么会有这种奇怪的结构?
一个重要原因是每个节点都有一个句柄。 这样我们就可以轻松地针对特定节点。 这在编辑文档或要链接到特定节点时非常方便。 请记住,Abmedium 不是文本媒体。 因此,我们需要一种方法来精确定位文档中的特定位置,就像您可以指向特定行中的特定字符一样。
另一个原因是它让我们可以表达树以外的其他图形。 上面的示例是一棵树,但假设我们要表达一个 DAG。
let dag = Automerge.change(document<{}>(), (doc) => {
doc.layers.base[0] = { label: 0, value: seq(1, 1) };
doc.layers.base[1] = { label: 1, value: str("same") };
});
如果我们想打印它,我们需要再次把它变成一棵树。
out = treeOf(proj(dag), stringPresenter);
console.log("2.", out);
// ⇒ 2. ["same", "same"]
该结构还适用于图层和投影。
顺便说一下,Abmedium 是建立在 Automerge 之上的。 如果它对您来说是新的,您可能应该在继续阅读之前看一下它。
Nodes and NodeValues
节点值有七种类型:nil、数字、引用、序列、字符串、符号和文本。 可以使用三个字母的函数创建 NodeValue:nil
、num
、ref
、seq
、str
、sym
或 txt
。 节点是带有一些额外信息的节点值。 它有一个标签、元数据和可能的跟踪值(稍后会详细介绍)。 可以使用 node
函数创建节点,如上例所示。 这非常冗长。 因此,有一些速记函数,以节点的 NodeValue 类型命名,并带有 n
后缀。 例如,字符串节点的简写是 strn
。
您可以编写 strn(0, "foo", {})
而不是 node(0, str("foo"), {})
。 我们将在其余示例中使用这些速记函数。
Layers and Projections
文档有层。 节点必须添加到图层中,不能直接添加到文档中。 到目前为止,我们只使用了一层,即基础层。 默认情况下,它是文档的一部分。 让我们再添加一层。
fruits = Automerge.change(fruits, (doc) => {
doc.layers.se = layer<{}>();
doc.compositions.se = {
label: "base",
layers: [{ label: "se" }],
};
});
在上面我们添加了一个名为 se 的层。 我们还添加了一个图层组合,用于定义图层之间的关系。 在这种情况下,我们指定 se 在 base 之上。
se 层应包含瑞典语内容。 让我们添加它。
fruits = Automerge.change(fruits, (doc) => {
doc.layers.se[4] = strn(4, "äpple", {});
doc.layers.se[6] = strn(6, "banan", {});
doc.layers.se[8] = strn(8, "päron", {});
});
为了使用瑞典语内容,我们投影 se 组合。 由于 se 在 base 之上,因此其节点将覆盖(覆盖)基本节点。
要创建投影,我们调用 proj
函数,该函数已在前面的示例中出现。 第一个参数是文档,第二个可选参数是组合。 如果未传递组合参数,则投影默认组合。 创建文档时,会将一个简单的默认合成添加到文档中。 它只包含基础层,但可以根据需要进行编辑。
让我们记录 se 组合。
out = pres(proj(fruits, fruits.compositions.se), stringPresenter);
console.log("3.", out);
// ⇒ 3. [["äpple", 1], ["banan", 2], ["päron", 3]]
如果您投射默认组合,英语字符串将像以前一样投射。
Disagreements
分歧是一种安全机制,可防止您将价值投射到意想不到的价值之上。 当你添加一个节点时,你也添加了它期望覆盖的值。 隐含这是未定义的。
我们已经制造了分歧。 se 的所有节点都期望覆盖一个未定义的值,但实际上它们覆盖了英文内容。 为什么隐藏分歧?
答案在于我们之前默默定义的 stringPresenter
函数。 此函数在传递给 treeOf
(TODO 重命名)时计算投影。 如果我们想显示分歧,我们需要传递一个不同的函数。 我们开始做吧。
const stringPresenter2: NodePresenter<{}, string> = presNodeswitch({
seq: (_, items) => `[${items.join(", ")}]`,
str: ({ value, disagreements }) => {
if (disagreements) {
const { expected, actual, to } = disagreements.se;
return `»${expected?.value} ≠ ${actual?.value} → ${to?.value}«`;
} else return `"${value}"`;
},
_: (n) => String(n.value),
});
然后将这个函数传递给pres
。
out = pres(proj(fruits, fruits.compositions.se), stringPresenter2);
console.log("4.", out);
// ⇒ 4. [[»undefined ≠ apple → äpple«, 1], [»undefined ≠ banana → banan«, 2], [»undefined ≠ pear → päron«, 3]]
现在,分歧按照我们在 stringPresenter2
中编程的方式呈现。
让我们解决分歧,既然我们已经证明它们存在。
fruits = Automerge.change(fruits, (doc) => {
doc.layers.se[4] = strn(4, "äpple", {}, str("apple"));
doc.layers.se[6] = strn(6, "banan", {}, str("banana"));
doc.layers.se[8] = strn(8, "päron", {}, str("pear"));
});
然后用预测的 se 组合调用 pres
以验证分歧是否消失。
out = pres(proj(fruits, fruits.compositions.se), stringPresenter2);
console.log("5.", out);
// ⇒ 5. [["äpple", 1], ["banan", 2], ["päron", 3]]
Simultaneities
除了分歧之外,Abmedium 还有同时性的概念。 当并发更新节点时,会创建同时性。 Abmedium 依靠 Automerge 来跟踪并发更新。
让我们从现有文档创建一个新文档。 在一个更现实的例子中,这个其他文档将存在于另一个设备(或至少另一个进程)上。
let fruits2 = Automerge.init<Document<{}>>();
fruits2 = Automerge.merge(fruits2, fruits);
现在更新原始文档。
fruits = Automerge.change(fruits, (doc) => {
doc.layers.se[4].value = "Äpple";
});
然后更新新文档。
fruits2 = Automerge.change(fruits2, (doc) => {
doc.layers.se[4].value = "ÄPPLE";
});
然后将原件与副本合并。
fruits = Automerge.merge(fruits, fruits2);
我们刚才所做的是同时更新文档。 这意味着两个文档都在不知道另一个文档更新的情况下进行了更新。 根据上下文,文档可能会持续数周而不知道其他文档中发生了什么。 或者他们可能知道文档是否在不到一毫秒内更新。 Abmedium 旨在处理这两种情况。 如果您在文档上紧密合作,在同一层工作可能是个好主意,尽管您可能想保护自己不受他人更改的影响,因此将自己置于子层中。 如果文档很少合并,则更改可能应该发生在不同的层中,然后可能合并到一个超层中。
与分歧一样,您需要更新 stringPresenter 函数,否则将随机选择其中一个同时出现的值,而其他值将被静默丢弃。
const stringPresenter3: NodePresenter<{}, string> = presNodeswitch({
seq: (_, items) => `[${items.join(", ")}]`,
str: ({ value, disagreements, simultaneities }) => {
if (disagreements) {
const { expected, actual, to } = disagreements.se;
return `»${expected?.value} ≠ ${actual?.value} → ${to?.value}«`;
} else if (simultaneities) {
return `{${Object.values(simultaneities)
.map(({ value }) => String(value))
.join(" ")}}`;
} else return `"${value}"`;
},
_: (n) => String(n.value),
});
现在,让我们根据 stringPresenter3
的定义,在大括号内呈现同时可见的文档。
out = pres(proj(fruits, fruits.compositions.se), stringPresenter3);
console.log("6.", out);
// ⇒ 6. [[{"Äpple" "ÄPPLE"}, 1], ["banan", 2], ["päron", 3]]
要摆脱同时性,只需再次更新有问题的节点即可。 在实际情况中,这意味着您首先看到了同时性,然后选择了您想要保留的值。
fruits = Automerge.change(fruits, (doc) => {
doc.layers.se[4].value = "Äpple";
});
再次运行 proj
和 pres
并验证同时性消失了。
out = pres(proj(fruits, fruits.compositions.se), stringPresenter3);
console.log("7.", out);
// ⇒ 7. [["Äpple", 1], ["banan", 2], ["päron", 3]]
Examples
检查 examples 目录以获取更多示例。 from-readme.js 包含本文档中的所有示例。
Maturity Status
这个包还不成熟。 它不稳定,会发生很大变化。 如果您想使用它会很棒,但要准备颠簸的旅程。 也许你想在这种情况下贡献?
Abmedium
Abmedium is a graph medium. Unlike text it can contain loops and nodes with multiple parents.
It is made for distributed, structured editing in multilingual environments. A node can have values in different layers. Layers can be put in layer compositions and projected. This way different projections can be created from one document. Projections can contain localized content, and emulate feature toggles/branches.
Layers can get in a conflict when they are projected in a stack. This is called a disagreement and is one of the two types of conflicts Abmedium handles. The other type is simultaneities. They occur when a node is edited concurrently.
Terminology
- Documents is a collection of layers and compositions.
- Layers are sets of nodes.
- All nodes have Labels. They can be numbers or strings.
- Nodes are values with a label.
- Values are sequences, strings, texts, symbols, numbers, refs or nil.
- Projections are created when a composition is projected.
- Layer Composition describes which layers, and in what order, to project.
- Disagreements are created during a projection. They represent a mismatch between an expected and actual value.
- Simultaneities are created when new values are added concurrently to the same node.
Document Structure
Let's put the content [["a", 1], ["b", 2], ["c", 3]]
into an Abmedium document. This is done by destructuring the content into nodes.
let fruits = Automerge.from(document<{}>());
fruits = Automerge.change(fruits, (doc) => {
const base = doc.layers.base;
base[0] = node(0, seq([1, 2, 3]), {});
base[1] = node(1, seq([4, 5]), {});
base[2] = node(2, seq([6, 7]), {});
base[3] = node(3, seq([8, 9]), {});
base[4] = node(4, str("apple"), {});
base[5] = node(5, num(1), {});
base[6] = node(6, str("banana"), {});
base[7] = node(7, num(2), {});
base[8] = node(8, str("pear"), {});
base[9] = node(9, num(3), {});
});
This is a very cumbersome format. Don't be frightened! The library is meant to be used in the background and not manipulated directly as in this document.
To make sure we have not lost our content structure, let's rebuild it!
const stringPresenter: NodePresenter<{}, string> = presNodeswitch({
seq: (_, items) => `[${items.join(", ")}]`,
str: (n) => `"${n.value}"`,
_: (n: PresNode<any, string>) => String(n.value),
});
let out = pres(proj(fruits), stringPresenter);
console.log("1.", out);
// ⇒ 1. [(["apple", 1], ["banana", 2], ["pear", 3])];
There is a lot going on here. To begin with, let's talk about the elephant in the room. Why this strange structure?
One big reason is that every node get a handle. This way we can easily target specific nodes. This is handy when the document is edited, or if you want to link to a specific node. Remember, Abmedium is not a text medium. Therefore we need a way to pinpoint a specific place in the document the way you can point to a specific character at a specific line.
Another reason is that it let's us express other graphs than trees. The example above is a tree, but let's say we want to express a DAG.
let dag = Automerge.change(document<{}>(), (doc) => {
doc.layers.base[0] = { label: 0, value: seq(1, 1) };
doc.layers.base[1] = { label: 1, value: str("same") };
});
If we want to print it we need to turn it to a tree again.
out = treeOf(proj(dag), stringPresenter);
console.log("2.", out);
// ⇒ 2. ["same", "same"]
The structure also works with layers and projections.
By the way, Abmedium is built on top of Automerge. If it is new for you, you should probably take a look at it before you continue to read.
Nodes and NodeValues
There are seven types of node values: nil, numbers, references, sequences, strings, symbols, and texts. A NodeValue can be created using a three-letter function: nil
, num
, ref
, seq
, str
, sym
, or txt
. A Node is a NodeValue with some extra information. It has a label, metadata and possibly tracked values (more about that later). A Node can be created using the node
function, as in the examples above. This is pretty verbose. Therefore there are some shorthand functions, named after the NodeValue type of the node with an n
-suffix. For example, the shorthand for a string node is strn
.
Instead of node(0, str("foo"), {})
you can write strn(0, "foo", {})
. We are going to use these shorthand functions in the rest of the examples.
Layers and Projections
A document has layers. Nodes must be added to a layer, and can not be added to the document directly. So far we have only worked with one layer, the base layer. It is part of a document by default. Let's add another layer.
fruits = Automerge.change(fruits, (doc) => {
doc.layers.se = layer<{}>();
doc.compositions.se = {
label: "base",
layers: [{ label: "se" }],
};
});
Above we add a layer called se. We also add a layer composition, which is used to define the relation between layers. In this case we specify that se is on top of base.
The se layer should contain Swedish content. Let's add it.
fruits = Automerge.change(fruits, (doc) => {
doc.layers.se[4] = strn(4, "äpple", {});
doc.layers.se[6] = strn(6, "banan", {});
doc.layers.se[8] = strn(8, "päron", {});
});
To use the Swedish content we project the se composition. Since se is on top of base its nodes will cover (overwrite) the base nodes.
To create a projection we call the proj
function, which already has been present in the previous examples. The first parameter is the document and the second optional parameter is the composition. If no composition argument is passed the default composition is projected. A simple default composition is added to a document when it is created. It only contains the base layer, but can be edited if you wish to.
Let's log the se composition.
out = pres(proj(fruits, fruits.compositions.se), stringPresenter);
console.log("3.", out);
// ⇒ 3. [["äpple", 1], ["banan", 2], ["päron", 3]]
If you project the default composition, the English strings will be projected as before.
Disagreements
A disagreement is a safety mechanism that prevents you from project a value over an unexpected value. When you add a node you also add the value it expects to cover. Implicitly this is undefined.
We have already created disagreements. All of the nodes of se expects to cover an undefined value, but in fact they cover English content. Why are the disagreements hidden?
The answer lies in the stringPresenter
function we previously, silently defined. This function evaluates a projection when passed to treeOf
(TODO rename). If we want to display disagreements we need to pass a different function. Let's do it.
const stringPresenter2: NodePresenter<{}, string> = presNodeswitch({
seq: (_, items) => `[${items.join(", ")}]`,
str: ({ value, disagreements }) => {
if (disagreements) {
const { expected, actual, to } = disagreements.se;
return `»${expected?.value} ≠ ${actual?.value} → ${to?.value}«`;
} else return `"${value}"`;
},
_: (n) => String(n.value),
});
Then pass this function to pres
.
out = pres(proj(fruits, fruits.compositions.se), stringPresenter2);
console.log("4.", out);
// ⇒ 4. [[»undefined ≠ apple → äpple«, 1], [»undefined ≠ banana → banan«, 2], [»undefined ≠ pear → päron«, 3]]
Now the disagreements are rendered the way we programmed in stringPresenter2
.
Let's fix the disagreements, now that we have been shown that they exist.
fruits = Automerge.change(fruits, (doc) => {
doc.layers.se[4] = strn(4, "äpple", {}, str("apple"));
doc.layers.se[6] = strn(6, "banan", {}, str("banana"));
doc.layers.se[8] = strn(8, "päron", {}, str("pear"));
});
Then call pres
with the projected se composition to verify the disagreemets are gone.
out = pres(proj(fruits, fruits.compositions.se), stringPresenter2);
console.log("5.", out);
// ⇒ 5. [["äpple", 1], ["banan", 2], ["päron", 3]]
Simultaneities
In addition to disagreements Abmedium also have the concept of simultaneities. A simultaneity is created when a node is updated concurrently. Abmedium relies on Automerge to keep track of concurrent updates.
Let's create a new document from the existing one. In a more realistic example this other document would live on another device (or at least another process).
let fruits2 = Automerge.init<Document<{}>>();
fruits2 = Automerge.merge(fruits2, fruits);
Now update the original document.
fruits = Automerge.change(fruits, (doc) => {
doc.layers.se[4].value = "Äpple";
});
Then update the new document.
fruits2 = Automerge.change(fruits2, (doc) => {
doc.layers.se[4].value = "ÄPPLE";
});
Then merge the original with the copy.
fruits = Automerge.merge(fruits, fruits2);
What we just did was to update the documents concurrently. It means both of the documents were updated without knowing the other one was updated. Depending on the context documents might go on for weeks without knowing what happens in other documents. Or they might know if a document is updated in less than a millisecond. Abmedium is designed to handle both contexts. If you cooperate tightly on a document, it is probably a good idea to work in the same layer, though you might want to shield yourself from others changes, and therefore place yourself in a sublayer. If documents are merged more seldom the changes should probably happen in different layers, and then perhaps merged into a superlayer.
Just as with disagreements you need to update the stringPresenter function, otherwise one of the simultaneous values will randomly be selected and the other ones silently discarded.
const stringPresenter3: NodePresenter<{}, string> = presNodeswitch({
seq: (_, items) => `[${items.join(", ")}]`,
str: ({ value, disagreements, simultaneities }) => {
if (disagreements) {
const { expected, actual, to } = disagreements.se;
return `»${expected?.value} ≠ ${actual?.value} → ${to?.value}«`;
} else if (simultaneities) {
return `{${Object.values(simultaneities)
.map(({ value }) => String(value))
.join(" ")}}`;
} else return `"${value}"`;
},
_: (n) => String(n.value),
});
Now let's render the document with the simultaneities visible inside of the curly brackets, as defined by stringPresenter3
.
out = pres(proj(fruits, fruits.compositions.se), stringPresenter3);
console.log("6.", out);
// ⇒ 6. [[{"Äpple" "ÄPPLE"}, 1], ["banan", 2], ["päron", 3]]
To get rid of the simultaneity just update the troublesome node again. In a real situation this means that you first have seen the simultaneity and then chosen the value you want to keep.
fruits = Automerge.change(fruits, (doc) => {
doc.layers.se[4].value = "Äpple";
});
Once again, run proj
and pres
and verify that the simultaneity is gone.
out = pres(proj(fruits, fruits.compositions.se), stringPresenter3);
console.log("7.", out);
// ⇒ 7. [["Äpple", 1], ["banan", 2], ["päron", 3]]
Examples
Inspect the examples directory for more examples. from-readme.js contains all the examples in this document.
Maturity Status
This package is not mature. It is not stable and will change a lot. It would be wonderful if you want to use it, but prepare for a bumpy ride. Maybe you want to contribute in that case?
你可能也喜欢
- 31i73-class 中文文档教程
- @0x4447/tomato 中文文档教程
- @2alheure/vue-accordion 中文文档教程
- @7digital/mysql2-timeout 中文文档教程
- @aaronuu/react-layout 中文文档教程
- @abrusca/casteachingalbert 中文文档教程
- @abstract-elements/abstract-elements-base-class 中文文档教程
- @accede-web/overlay 中文文档教程
- @activimetrics/utils-composite 中文文档教程
- @actyx-contrib/ng-pond 中文文档教程