Blocs et schéma

À l'exécution, le document est une carte plateMap<BlockId, Block> plus des références d'enfants ; au repos, c'est un arbre JSON imbriqué. La carte plate rend les recherches et les modifications en O(1) ; l'arbre rend le fichier lisible par un humain.

interface Block {
  id: BlockId;          // UUIDv7 — triable par date de création
  type: string;         // 'paragraph', 'heading', 'table_cell', le vôtre…
  version: number;      // versionné par type, pour les migrations
  props: Record<string, unknown>;
  text?: Run[];         // texte enrichi, seulement pour les blocs "inline"
  children: BlockId[];
  parentId: BlockId | null;
}

Le registre de schéma

Un type de bloc est déclaré par une spécification sérialisable en JSON. C'est délibéré : le rendu statique et un futur portage Swift consomment la même déclaration sans exécuter le moindre comportement JavaScript.

s.register({
  type: 'callout',
  version: 1,
  inline: true,                      // porte du texte enrichi
  defaultProps: { icon: '💡' },
  placeholder: 'Écris quelque chose…',
});

Trois catégories

  • text — porte du texte, donc un caret : paragraphe, titre, élément de liste.
  • void — pas de texte à éditer : image, séparateur, lien de page. On l'attrape, on ne l'édite pas.
  • layout — de la structure : colonnes, lignes de tableau, la racine. Jamais une cible de glisser-déposer en soi.

Cette catégorie n'est pas cosmétique : elle décide si un clic place un caret ou sélectionne le bloc, et si un pointeur qui descend démarre une sélection de texte ou un déplacement.