ガイド: デルタフォーマットの設計 Github で編集する

デルタフォーマットの設計

リッチ テキスト エディターには、独自のコンテンツを表現するための仕様がありません。最近まで、ほとんどのリッチ テキスト エディターは、独自の編集領域に何があるかさえ知りませんでした。これらのエディタは、ユーザーに HTML を渡すだけであり、これを解析して解釈する負担も伴います。この解釈は常に、主要なブラウザ ベンダーの解釈とは異なるため、ユーザーの編集エクスペリエンスが異なります。

Quill は、それ自体の内容を実際に理解できる最初のリッチ テキスト エディターです。これの鍵となるのは、リッチ テキストを記述する仕様である Deltas です。デルタは、理解しやすく、使いやすいように設計されています。デルタの背後にある考え方をいくつか見ていき、以下に光を当てます。なぜ物事はそのとおりです。

リファレンスをお探しの場合は、デルタとは、デルタのドキュメントはより簡潔なリソースです。

プレーンテキスト

プレーンテキストだけで基本から始めましょう。プレーンテキストを保存するためのユビキタスな形式、つまり文字列がすでに存在します。これに基づいて、範囲が太字になっている場合など、書式設定されたテキストを記述したい場合は、追加情報を追加する必要があります。

使用できるその他の順序付きデータ型は配列だけであるため、オブジェクトの配列を使用します。これにより、さまざまなツールとの互換性のために JSON を活用することもできます。

var content = [
  { text: 'Hello' },
  { text: 'World', bold: true }
];

必要に応じて、斜体、下線、その他の形式をメイン オブジェクトに追加できます。でも分けたほうがすっきりしますtextこれらすべてから、書式設定を 1 つのフィールドの下に整理します。このフィールドに名前を付けます。attributes

var content = [
  { text: 'Hello' },
  { text: 'World', attributes: { bold: true } }
];

コンパクト

これまでの単純なデルタ形式でも、上記の「Hello World」の例は別の方法で表現できるため、どのものが生成されるかを予測できません。

var content = [
  { text: 'Hel' },
  { text: 'lo' },
  { text: 'World', attributes: { bold: true } }
];

これを解決するには、デルタがコンパクトでなければならないという制約を追加します。この制約がある場合、上記の表現は有効なデルタではありません。これは、「Hel」と「lo」が分離されていなかった前の例でよりコンパクトに表現できるためです。同様に、{ bold: false, italic: true, underline: null }、 なぜなら{ italic: true }よりコンパクトになります。

正規

私たちは何の意味も割り当てていませんbold、テキストの書式設定について説明しているだけです。次のような別の名前を使用することもできたはずです。weightedまたstrong、または、重みの数値範囲または記述範囲など、可能な値の異なる範囲を使用しました。 CSS には例があり、これらのあいまいさのほとんどが影響しています。ページ上に太字のテキストが表示された場合、そのルール セットが次のとおりであるかどうかを予測することはできません。font-weight: boldまたfont-weight: 700。これにより、CSS を解析してその意味を識別するタスクがさらに複雑になります。

可能な属性のセットやその意味は定義しませんが、デルタが正規でなければならないという追加の制約を追加します。 2 つのデルタが等しい場合、それらが表すコンテンツも等しい必要があり、同じコンテンツを表す 2 つの異なるデルタがあってはなりません。これにより、プログラム的に 2 つのデルタを単純に詳細に比較して、それらが表すコンテンツが等しいかどうかを判断できます。

したがって、次のようなことがあった場合、私たちが導き出せる唯一の結論は次のとおりです。aとは異なりますb、でも何ではないaまたb意味。

var content = [{
  text: "Mystery",
  attributes: {
    a: true,
    b: true
  }
}];

適切な名前を選択するのは実装者次第です。

var content = [{
  text: "Mystery",
  attributes: {
    italic: true,
    bold: true
  }
}];

この正規化はキーと値の両方に適用されます。textattributes。たとえば、Quill はデフォルトで次のようになります。

  • RGB ではなく、6 文字の 16 進数値を使用して色を表現します
  • 改行を表す方法は 1 つだけです。\n、 いいえ\rまた\r\n
  • text: "Hello  World"これは、「Hello」と「World」の間に正確に 2 つのスペースがあることを明確に意味します。

これらの選択肢の一部はユーザーがカスタマイズできますが、デルタの標準的な制約により、選択肢は一意である必要があります。

この明確な予測可能性により、処理するケースが少なくなり、対応するデルタがどのようになるかについて驚くことがないため、デルタの操作が容易になります。長期的には、これにより、デルタを使用するアプリケーションの理解と保守が容易になります。

行の書式設定

行フォーマットは行全体の内容に影響を与えるため、コンパクトで標準的な制約に対して興味深い課題を提示します。テキストを中央揃えで表現する合理的と思われる方法は、次のとおりです。

var content = [
  { text: "Hello", attributes: { align: "center" } },
  { text: "\nWorld" }
];

しかし、ユーザーが改行文字を削除した場合はどうなるでしょうか?単純に改行文字を削除すると、デルタは次のようになります。

var content = [
  { text: "Hello", attributes: { align: "center" } },
  { text: "World" }
];

この線はまだ中央にありますか?答えが「いいえ」の場合、属性オブジェクトは必要なく、2 つの文字列を結合できるため、この表現はコンパクトではありません。

var content = [
  { text: "HelloWorld" }
];

しかし、答えが「はい」の場合、align 属性を持つ文字の順列は同じ内容を表すことになるため、正規制約に違反します。

したがって、単純に改行文字を取り除くことはできません。また、行属性を削除するか、行上のすべての文字を埋めるように拡張する必要があります。

以下から改行を削除したらどうなるでしょうか?

var content = [
  { text: "Hello", attributes: { align: "center" } },
  { text: "\n" },
  { text: "World", attributes: { align: "right" } }
];

結果の線が中央に揃えられているのか、それとも右に揃えられているのかは不明です。両方を削除することも、一方を他方よりも優先する何らかの順序付けルールを設けることもできますが、デルタはより複雑になり、このパスでの作業が困難になります。

この問題には原子性が必要であり、これは改行キャラクターそのもの。しかし、次の点で 1 つずれているという問題があります。nライン、私たちにはそれしかありませんn-1改行文字。

これを解決するために、Quill はすべてのドキュメントに改行を「追加」し、デルタを常に「\n」で終了します。

// Hello World on two lines
var content = [
  { text: "Hello" },
  { text: "\n", attributes: { align: "center" } },
  { text: "World" },
  { text: "\n", attributes: { align: "right" } }   // Deltas must end with newline
];

埋め込みコンテンツ

画像やビデオなどの埋め込みコンテンツを追加したいと考えています。文字列はテキストに使用するのが自然ですが、埋め込みにはさらに多くのオプションがあります。埋め込みにはさまざまなタイプがあるため、選択にはこのタイプの情報を含めてから実際のコンテンツを含めるだけで済みます。ここには多くの適切なオプションがありますが、唯一のキーが埋め込みタイプであり、値がコンテンツ表現であるオブジェクトを使用します。このオブジェクトは任意のタイプまたは値を持つことができます。

var img = {
  image: {
    url: 'https://quilljs.com/logo.png'
  }
};

var f = {
  formula: 'e=mc^2'
};

テキストと同様に、画像にもいくつかの決定的な特徴と、いくつかの一時的な特徴がある場合があります。私たちが使用したattributesテキストコンテンツにも同じものを使用できますattributes画像のフィールド。ただし、このため、これまで使用してきた一般的な構造を維持できますが、名前を変更する必要があります。textもっと一般的なものに鍵をかけてください。理由については後ほど説明しますので、名前を選択します。insert。これをすべてまとめると次のようになります。

var content = [{
  insert: 'Hello'
}, {
  insert: 'World',
  attributes: { bold: true }
}, {
  insert: {
    image: 'https://exclamation.com/mark.png'
  },
  attributes: { width: '100' }
}];

変更の説明

デルタという名前が示すように、私たちの形式は文書自体だけでなく文書への変更も記述することができます。実際、ドキュメントは、記述している内容を得るために空のドキュメントに加える変更と考えることができます。すでにご想像のとおり、変更の説明にもデルタを使用することが、名前を変更した理由です。textinsertついさっき。デルタ配列の各要素をオペレーションと呼びます。

消去

テキストの削除について説明するには、削除する場所と文字数を知る必要があります。埋め込みを削除するには、埋め込みの長さを理解すること以外に特別な処理は必要ありません。 1 つ以外の場合は、埋め込みの一部のみが削除された場合に何が起こるかを指定する必要があります。現時点ではそのような仕様はないため、画像を構成するピクセル数、ビデオの長さ、デッキ内のスライドの数に関係なく、埋め込みはすべての長さです

削除を記述する合理的な方法の 1 つは、そのインデックスと長さを明示的に保存することです。

var delta = [{
  delete: {
    index: 4,
    length: 1
  }
}, {
  delete: {
    index: 12,
    length: 3
  }
}];

インデックスに基づいて削除を順序付けし、範囲が重複していないことを確認する必要があります。そうしないと、正規の制約に違反してしまいます。このインデックスと長さのアプローチには他にも欠点がいくつかありますが、形式の変更を説明した後で理解しやすくなります。

入れる

デルタは空ではないドキュメントへの変更を記述している可能性があるため、{ insert: "Hello" }「Hello」をどこに挿入すればよいかわからないため、これでは不十分です。これは、次のようにインデックスを追加することで解決できます。delete

フォーマット

削除と同様に、フォーマットの変更自体とともに、フォーマットするテキストの範囲を指定する必要があります。書式設定はattributesしたがって、簡単な解決策は、追加のオブジェクトを提供することです。attributes既存のオブジェクトとマージするオブジェクト。物事をシンプルにするために、このマージは浅いものになっています。深いマージを必要とするほど説得力があり、複雑さがさらに増すことになるユースケースはまだ見つかっていません。

var delta = [{
  format: {
    index: 4,
    length: 1
  },
  attributes: {
    bold: true
  }
}];

特殊なケースは、書式設定を削除する場合です。我々は使用するだろうnullこの目的のために、だから{ bold: null }太字形式を削除することを意味します。任意の偽の値を指定することもできましたが、属性値を正当な使用例で指定できる可能性があります。0または空の文字列。

ノート:ここで、アプリケーション層のインデックスに注意する必要があります。前述したように、デルタは、どのようなものにも固有の意味を与えません。attributes' キーと値のペア、埋め込みタイプや値はありません。デルタは、画像には長さがなく、テキストには代替テキストがなく、ビデオは太字にできないことを認識しません。以下は、法的他の適用の結果生じた可能性のあるデルタ法的アプリケーションが形式の範囲に注意を払っていないことによるデルタ。

var delta = [{
  insert: {
    image: "https://imgur.com/"
  },
  attributes: {
    duration: 600
  }
}, {
  insert: "Hello",
  attributes: {
    alt: "Funny cat photo"
  }
}, {
  insert: {
    video: "https://youtube.com/"
  },
  attributes: {
    bold: true
  }
}];

落とし穴

まず、インデックスはドキュメント内の位置を参照する必要があることを明確にする必要があります。any 操作が適用されます。そうしないと、後の操作で以前の挿入が削除されたり、以前のフォーマットがアンフォーマットされたりする可能性があり、コンパクト性に違反することになります。

正規の制約を満たすために、操作も厳密に順序付けする必要があります。これを実現する有効な方法の 1 つは、インデックス、次に長さ、次にタイプの順に並べることです。

前述したように、削除範囲は重複できません。形式範囲の重複に対する訴訟はそれほど簡潔ではありませんが、形式の重複も望ましくないことがわかりました。

デルタが無効である可能性がある理由は山積しています。より良い形式では、そのようなケースをまったく表現できなくなります。

保持

コンパクト化の形式から少し離れて考えると、挿入、削除、および書式設定を表現するためのはるかに単純な形式を記述することができます。

  • デルタには、少なくとも変更されるドキュメントと同じ長さの操作が含まれます。
  • 各オペレーションは、そのインデックスでキャラクターに何が起こるかを記述します。
  • オプションの挿入操作により、デルタが記述されているドキュメントよりも長くなる可能性があります。

これには新しいオペレーションの作成が必要になります。これは単に「このキャラクターをそのままにしておく」ことを意味します。私たちはこれを と呼んでいますretain

// Starting with "HelloWorld",
// bold "Hello", and insert a space right after it
var change = [
  { format: true, attributes: { bold: true } },  // H
  { format: true, attributes: { bold: true } },  // e
  { format: true, attributes: { bold: true } },  // l
  { format: true, attributes: { bold: true } },  // l
  { format: true, attributes: { bold: true } },  // o
  { insert: ' ' },
  { retain: true },  // W
  { retain: true },  // o
  { retain: true },  // r
  { retain: true },  // l
  { retain: true }   // d
]

すべての文字が記述されるため、明示的なインデックスと長さは必要なくなります。これにより、重複する範囲や順序が乱れたインデックスを表現することができなくなります。

したがって、隣接する同等の操作をマージする簡単な最適化を行うことができます。長さ。最後の操作がretainこれは単に「ドキュメントの残りの部分には何もしない」ように指示するだけなので、単純に削除することができます。

var change = [
  { format: 5, attributes: { bold: true } }
  { insert: ' ' }
]

さらに、次のことに気づくかもしれません。retainそれはある意味で単なる特殊なケースですformat。たとえば、実際には次のような違いはありません。{ format: 1, attributes: {} }{ retain: 1 }。圧縮すると空のものが削除されますattributesオブジェクトは私たちにただ残します{ format: 1 }、正規化の競合が発生します。したがって、この例では単純に組み合わせます。formatretain、名前を保持しますretain

var change = [
  { retain: 5, attributes: { bold: true } },
  { insert: ' ' }
]

現在の標準フォーマットに非常に近いデルタが完成しました。

作戦

現在、リッチ テキストを記述する使いやすい JSON 配列が用意されています。これはストレージ層とトランスポート層では優れていますが、アプリケーションはさらに多くの機能を活用できる可能性があります。これを追加するには、Delta をクラスとして実装し、JSON から簡単に初期化したり、JSON にエクスポートしたりして、それに関連するメソッドを提供します。

Delta の開始時点では、配列をサブクラス化することはできませんでした。このため、デルタは単一のプロパティを持つオブジェクトとして表現されます。opsこれには、これまで説明してきたようなオペレーションの配列が格納されます。

var delta = {
  ops: [{
    insert: 'Hello'
  }, {
    insert: 'World',
    attributes: { bold: true }
  }, {
    insert: {
    image: 'https://exclamation.com/mark.png'
    },
    attributes: { width: '100' }
  }]
};

最後に、私たちはに到着しますデルタ形式、今日存在しているように。


オープンソースプロジェクト

Quill は次によって開発および保守されています。スラブ。 BSD の下で寛容にライセンスされています。個人または商業プロジェクトで自由に使用してください。
8,000