Skip to content

Canonical Editable HTML Strategy

유용태 edited this page Jun 24, 2026 · 1 revision

Canonical Editable HTML 전략

이 문서는 Bear급 note app을 만들 수 있는 core engine을 목표로 할 때, HTML, Markdown, DOM, JSON model의 책임을 어떻게 나눌지 고정한다.

결론

HTML은 표준이지만 core model의 source of truth로 삼지 않는다.

Core model은 typed JSON document다. HTML은 그 model에서 투영된 canonical editable dialect일 때만 lossless reverse mapping 대상이다.

Typed JSON document model
  -> canonical editable HTML
  -> contenteditable DOM
  -> DOM observation
  -> typed operation / JSON Patch
  -> Typed JSON document model

외부 HTML은 이 경로에 바로 들어오지 않는다. 외부 HTML은 importer/sanitizer가 best-effort로 typed JSON model로 변환한다.

왜 HTML 전체가 source of truth가 아닌가

HTML은 같은 의미를 여러 방식으로 표현할 수 있다.

<strong>text</strong>
<b>text</b>
<span style="font-weight: 700">text</span>
<span class="bold">text</span>

이 HTML을 모두 lossless하게 같은 model로 되돌리려면 sanitizer, schema, id registry, selection mapping, command semantics를 HTML 위에 다시 만들어야 한다. 그 순간 HTML이 source of truth가 아니라 HTML 안에 숨긴 별도 document model이 된다.

그래서 core가 보장할 것은 임의의 HTML round-trip이 아니다. Core가 보장할 것은 우리가 렌더한 canonical editable HTML dialect의 round-trip이다.

네 가지 truth의 책임

Layer 책임 Core truth 여부
Typed JSON model block, text surface, range, atom, fragment, operation 의미론
Canonical editable HTML contenteditable이 조작할 수 있는 stable projection 아니오
DOM native input, IME, selection, clipboard event transport 아니오
Markdown authoring shortcut, import/export, interchange format 아니오

Markdown 호환은 목표지만 Markdown editor가 목표는 아니다. Markdown은 typed model로 들어오고 typed model에서 나가는 format이다.

Canonical editable HTML dialect

Canonical editable HTML은 제한된 HTML subset과 data-* anchor를 가진다.

<section data-editable-document="note-1">
  <h1
    data-editable-block="block-title"
    data-editable-block-type="heading"
    data-editable-heading-level="1"
    data-json-text="/blocks/0/text"
  >
    Hello <strong data-editable-mark="bold">world</strong>
    <span
      data-json-atom="tag-1"
      data-editable-atom-type="tag"
      contenteditable="false"
    >#tag</span>
  </h1>
</section>

필수 anchor:

Anchor 의미
data-editable-document document identity
data-editable-block stable block id
data-editable-block-type block variant
data-json-text json-document text path
data-json-atom atom id
data-editable-atom-type atom variant
data-editable-mark inline range/mark projection

data-json-textdata-json-atom은 현재 contenteditable-web package의 public contract와 맞춘다.

Reverse mapping 보장 범위

Reverse mapping은 두 종류로 나뉜다.

우리가 렌더한 DOM

Lossless reverse mapping 대상이다.

  • block은 data-editable-block으로 model block id에 연결한다.
  • text surface는 data-json-text로 model string path에 연결한다.
  • atom은 data-json-atom을 만나면 model text에서 \uFFFC 한 글자로 계산한다.
  • mark는 canonical tag와 data-editable-mark를 range sidecar로 환원한다.
  • DOM selection은 text surface offset으로 환원한다.

외부 HTML

Importer/sanitizer 대상이다.

  • semantic tag는 가능한 한 typed block/range/atom으로 normalize한다.
  • unknown style/class/span은 기본적으로 버린다.
  • unsupported structure는 plain text나 fallback block으로 낮춘다.
  • lossless 보장을 하지 않는다.

Core model에 반드시 있어야 할 것

Bear급 note app을 만들 수 있으려면 core model에는 기능이 아니라 공통 표현 단위가 있어야 한다.

  1. Stable identity

    • document id
    • block id
    • atom id
    • range id
  2. Block model

    • paragraph
    • heading level 1~6
    • list item: bullet, ordered, task
    • quote
    • code block
    • extension block slot
  3. Text surface

    • text path
    • atoms sidecar path
    • ranges sidecar path
    • atom은 한 글자 offset으로 계산
  4. Inline range

    • bold
    • italic
    • underline
    • strike
    • code
    • highlight
    • link with payload
  5. Atom

    • mention
    • tag
    • wiki link
    • attachment/image placeholder
    • model text에서는 \uFFFC
  6. Fragment

    • selected text
    • selected atoms
    • selected ranges
    • selected blocks
    • plain text / HTML / Markdown fallback 가능
  7. Structural operation

    • split block
    • merge block
    • set/toggle block type
    • wrap/unwrap list
    • indent/outdent
    • toggle range
    • insert/remove atom
    • paste fragment
    • 모든 operation은 selectionAfter를 계산한다.

지금 core에 넣지 않을 것

다음은 Bear급 앱에는 필요하지만 core model의 불변부가 아니다.

  • toolbar
  • theme
  • Markdown parser policy
  • Markdown shortcut keymap
  • search index
  • backlink index
  • file storage
  • sync
  • encryption
  • export UI
  • visual line geometry

이들은 typed model 위의 package, adapter, host app에서 조립한다.

Package 방향

json-document 본체는 JSON editing foundation이다. Rich note model은 본체에 넣지 않고 공식 extension package로 둔다.

후보 이름:

@interactive-os/json-document-rich-text

이 package는 DOM을 요구하지 않는다. DOM과 contenteditable은 contenteditable-web adapter가 맡는다.

json-document
  -> rich-text model/operations
  -> canonical editable HTML codec
  -> contenteditable-web adapter
  -> host app

성공 기준

첫 안정화 목표는 다음 round-trip이다.

typed document
  -> canonical editable HTML
  -> DOM observation
  -> typed document patch
  -> same typed document semantics

동일해야 하는 것은 HTML string이 아니다. 동일해야 하는 것은 block id, text, atom offset, range start/end, selection 의미론이다.

이 기준을 만족하지 못하면 DOM bridge는 다시 임의 DOM 해석기가 되고, legacy editor와 같은 실패 경로로 돌아간다.