Skip to content

Latest commit

 

History

History
343 lines (253 loc) · 10.4 KB

File metadata and controls

343 lines (253 loc) · 10.4 KB

json-document Extensions

json-document core는 JSON editing foundation만 소유합니다. UI, persistence, system clipboard, collection command, stable id lookup 같은 앱별 책임은 extension이나 host code에서 조립합니다.

Extension은 core에 plugin으로 등록하지 않습니다. public JSONDocument surface를 받아 함수로 compose합니다.

설치

필요한 package만 설치합니다.

npm install @interactive-os/json-document @interactive-os/json-document-collection

공식 extension

공식 extension 목록과 lab 후보 목록은 repo catalog에서 생성됩니다. packages/*에 있는 publishable @interactive-os/json-document-* package가 공식 extension이고, labs/extensions/*는 후보입니다. public 문서에서 lab package를 공식 extension이라고 부르지 않습니다.

Lab package는 제품 feature 압력을 검증하기 위한 실험입니다. 설치 가능한 공식 package로 안내하지 않고, 제품별 조합은 Recipes에서 먼저 확인합니다.

선택 기준

상황 먼저 쓰는 표면
한 위치를 정확히 바꿈 core doc.insert, doc.replace, doc.delete, doc.move
ordered array item UX @interactive-os/json-document-collection
outliner promote/demote @interactive-os/json-document-outline
JSONPath 결과 일괄 변경 @interactive-os/json-document-bulk-edit
temporary invalid form input @interactive-os/json-document-form-draft
protected JSON Pointer edit guard @interactive-os/json-document-protected-ranges
reusable JSON payload insertion @interactive-os/json-document-snippets
저장됨/dirty 표시 @interactive-os/json-document-dirty-state
local draft save/restore @interactive-os/json-document-persist-web
browser clipboard I/O @interactive-os/json-document-clipboard-web
stable id를 현재 Pointer로 해석 @interactive-os/json-document-id-resolver
apply 전 patch dry-run @interactive-os/json-document-patch-preview
JSON string field search/replace @interactive-os/json-document-search-replace
sibling item structural group/ungroup @interactive-os/json-document-grouping
proposed patch accept/reject @interactive-os/json-document-proposed-changes
review comments anchored to Pointer @interactive-os/json-document-comments
product search ranking, focus, keyboard, rendered value 검색 host app

제품별 fit

제품 패턴 맞는 extension host가 남기는 책임
spreadsheet tabs/order collection, persist-web, dirty-state, bulk-edit, search-replace grid selection, formula/rendered-value search, TSV clipboard
rich editor JSON truth layer persist-web, dirty-state, collection ProseMirror/DOM selection adapter, markdown/parser semantics, editor command 이름
outliner rows outline, collection, clipboard-web, persist-web focus recovery, keyboard policy, default node factory
object or card id commands id-resolver, collection, dirty-state id generation, routing, selection UI
import/review dry-run patch-preview, proposed-changes, patch-log, dirty-state visual diff, approval workflow, storage
review/copy cleanup search-replace, comments review workflow, UI thread state, publish policy

오해 방지

이름 의미하지 않는 것
clipboard-web TSV/CSV spreadsheet clipboard engine
schema-form rendered form UI
grouping Airtable group-by view
calculated-fields formula language/runtime
protected-ranges 2D grid selection UI, auth, role model, or server authorization
drag-drop DOM drag/drop event handling
persist-web server sync
patch-log product activity feed
id-resolver id generator or relation graph
patch-preview visual diff or approval workflow
search-replace regex engine, rendered text extraction, or search UI
proposed-changes slash or mention autocomplete
comments rendered comment UI, collaboration, moderation, or persistence
form-draft rendered form components, masking, focus, or parser library
snippets slash command UI, palette search, parser, or id generation policy

Rich editor host pattern

ProseMirror 같은 editor는 DOM/contenteditable state를 소유하고, json-document document는 저장할 JSON truth layer를 소유합니다.

const persistence = createDocumentPersistence(doc, { key: "article-draft" });

editorView.dispatch(editorTransaction);
doc.commit([
  { op: "replace", path: "/doc", value: prosemirrorToJson(editorView.state.doc) },
], { label: "edit rich text", origin: "prosemirror" });

await persistence.save();

Editor selection, schema-specific parsing, Markdown/HTML serialization, IME handling은 host editor 책임입니다. json-document는 최종 JSON payload의 validation, persistence, dirty state, undo/redo boundary를 조립합니다.

collection

import { createCollection } from "@interactive-os/json-document-collection";

const collection = createCollection(doc);

collection.moveAfter("/lists/0/cards/0", "/lists/1/cards/0");
collection.duplicateAfter("/slides/0", {
  rekey: { fields: ["id"], strategy: "suffix" },
});
collection.deleteItems(["/tabs/1", "/tabs/3"]);

outline

import { createOutline } from "@interactive-os/json-document-outline";

const outline = createOutline(doc);

outline.demote("/children/1");
outline.promote("/children/0/children/1");

schema-form

import { createSchemaForm } from "@interactive-os/json-document-schema-form";

const form = createSchemaForm(doc, "/settings");

if (form.ok) {
  const title = form.fields.find((field) => field.key === "title");
  title?.set("Published");
}

form-draft

import { createFormDraft } from "@interactive-os/json-document-form-draft";

const drafts = createFormDraft(doc, {
  parse({ input }) {
    const value = Number(input);
    return Number.isFinite(value)
      ? { ok: true, value }
      : { ok: false, reason: "not a number" };
  },
});

drafts.set("/settings/count", "12");

if (drafts.canCommit("/settings/count").ok) {
  drafts.commit("/settings/count");
}

protected-ranges

import { createProtectedRanges } from "@interactive-os/json-document-protected-ranges";

const protectedRanges = createProtectedRanges(doc, [
  { id: "published-slug", pointer: "/slug", label: "Published slug" },
]);

if (protectedRanges.canReplace("/slug", "next").ok) {
  protectedRanges.replace("/slug", "next");
}

snippets

import { createSnippets } from "@interactive-os/json-document-snippets";

const snippets = createSnippets(doc, [
  {
    id: "todo-card",
    label: "Todo card",
    payload: { id: "todo", title: "New card", done: false },
  },
]);

if (snippets.canInsert("todo-card", "/cards/-", {
  rekey: { fields: ["id"], strategy: "suffix" },
}).ok) {
  snippets.insert("todo-card", "/cards/-", {
    rekey: { fields: ["id"], strategy: "suffix" },
  });
}

dirty-state

import { createDirtyState } from "@interactive-os/json-document-dirty-state";

const dirty = createDirtyState(doc);

dirty.isDirty();
dirty.markClean();

bulk-edit

import { createBulkEdit } from "@interactive-os/json-document-bulk-edit";

const bulk = createBulkEdit(doc);

if (bulk.canReplaceAll("$.items[*].done", true).ok) {
  bulk.replaceAll("$.items[*].done", true);
}

search-replace

import { createSearchReplace } from "@interactive-os/json-document-search-replace";

const text = createSearchReplace(doc);

const matches = text.find("draft", {
  include: ({ pointer }) => pointer.endsWith("/title") || pointer.endsWith("/body"),
});

if (matches.ok) {
  text.replaceMatch({
    pointer: matches.matches[0].pointer,
    range: matches.matches[0].ranges[0],
  }, "published");
}

search-replace owns literal JSON string-field find/replace, stale match checks, schema preflight, and atomic document mutation. Regex, fuzzy search, stemming, tokenization, rendered text extraction, match ranking, and advanced search UI are host-owned. If a host computes a regex match range for a current JSON string field, it can still pass { pointer, range } to canReplaceMatch or replaceMatch; regex replace-all planning and capture substitution remain host-owned.

proposed-changes

import { createProposedChanges } from "@interactive-os/json-document-proposed-changes";

const proposedChanges = createProposedChanges(doc);

proposedChanges.propose({
  id: "rename-title",
  operations: { op: "replace", path: "/title", value: "Reviewed" },
});

if (proposedChanges.canAccept("rename-title").ok) {
  proposedChanges.accept("rename-title", { label: "accept proposed change" });
}

comments

import { createComments } from "@interactive-os/json-document-comments";

const comments = createComments(doc);

comments.add({
  id: "review-title",
  pointer: "/title",
  text: "Needs a clearer title",
});

comments.resolve("review-title");

patch-log

import { createPatchLog } from "@interactive-os/json-document-patch-log";

const log = createPatchLog(doc);

doc.replace("/title", "Next");
log.replayInto(otherDoc);

persist-web

import { createDocumentPersistence } from "@interactive-os/json-document-persist-web";

const persistence = createDocumentPersistence(doc, { key: "draft" });

await persistence.save();
await persistence.restore({ restoreSelection: true });

const watch = persistence.watch();
doc.replace("/title", "Draft");
await watch.flush();
watch.stop();

patch-preview

import { createPatchPreview } from "@interactive-os/json-document-patch-preview";

const previewer = createPatchPreview(Schema, doc);
const preview = previewer.preview([
  { op: "replace", path: "/title", value: "Next" },
]);

if (preview.ok) {
  preview.value;
  preview.applied;
}

id-resolver

import { createIdResolver } from "@interactive-os/json-document-id-resolver";

const ids = createIdResolver(doc, {
  scopes: [
    {
      scope: "card",
      query: "$.columns[*].cards[*]",
      readId: (value) => isCard(value) ? value.id : undefined,
    },
  ],
});

ids.resolve("card", "card-1");
ids.current();

clipboard-web

import { createWebClipboard } from "@interactive-os/json-document-clipboard-web";

const webClipboard = createWebClipboard(doc);

await webClipboard.copy("/lists/0/cards/0");
await webClipboard.paste("/lists/1/cards/-", {
  rekey: { fields: ["id"], strategy: "suffix" },
});