NABI NOTE
문서

커스텀 날개 만들기

날개(wing)는 객체 하나입니다. 클래스를 상속하지도, 별도의 등록 절차를 밟지도 않습니다 — wings 배열에 넣는 것이 곧 등록입니다.

굵게·표·업로드도 여기 적힌 칸만 채워서 만들어져 있고, 코어 기능으로 들어가는 문도 꽃(flower) 하나입니다. 직접 만든 날개는 기본 날개와 같은 조건에서 동작합니다.


가장 짧은 날개

ts
import { nabi, markIcon } from 'nabi-note'
import type { NabiWing } from 'nabi-note'

function kbd(): NabiWing {
  return {
    id: 'text-kbd',
    group: 'mark',
    names: { ko: '키 이름', en: 'Key' },
    icon: markIcon('<rect x="1.75" y="4.75" width="12.5" height="6.5" rx="1.5"/>'),
    inline: {
      tag: 'kbd',
      attributes: [],
      escapeKeys: ['Escape'],
      claims: (element) => element.tagName === 'KBD',
    },
  }
}

nabi.create('#editor', { wings: [kbd()] })

툴바 버튼과 @ 메뉴 항목이 생기고, <kbd> 가 문서에서 살아남습니다. 등록하지 않으면 <kbd> 는 껍데기가 벗겨져 평문(plain text)으로 떨어집니다.


인스턴스가 아니라 팩토리 함수(factory)를 내보내세요

날개는 bold() 처럼 부를 때마다 새 객체를 돌려주는 함수로 만듭니다. 한 페이지에 에디터가 여럿 있을 수 있고, 객체 하나를 두 에디터가 나눠 가지면 그 안에 담긴 상태가 섞입니다.

ts
// ✅ 에디터마다 독립된 객체
export function kbd(): NabiWing { return { /* … */ } }

// ❌ 모듈 하나에 객체 하나 — 두 에디터가 같은 것을 씁니다
export const kbd: NabiWing = { /* … */ }

상태가 필요하면 팩토리 함수의 클로저에 둡니다. 업로드 날개가 그렇게 되어 있습니다 — 진행 중인 업로드 장부와 AbortController 가 전부 createWing() 안쪽 지역 변수입니다.

ts
export function counter(): NabiWing {
  let editor: EditorHandle | null = null   // 이 에디터만의 상태

  return {
    id: 'demo-counter',
    names: { ko: '글자 수', en: 'Count' },
    setup(handle) {
      editor = handle
      return () => { editor = null }
    },
  }
}

옵션을 받는 날개도 같은 모양입니다 — heading(1)·link({ allowLocalUrls: true })· code({ highlight }) 가 전부 인자를 받는 팩토리 함수입니다.


등록과 순서

ts
nabi.create('#editor', {
  wings: [bold(), italic(), highlight()],
  html: '<p>초기 내용</p>',
  onChange: (html) => console.log(html),
})

배열 순서가 곧 스캔 순서입니다. 어떤 마크업의 주인을 가릴 때 코어는 이 순서대로 claims 를 물어보고, 처음 true 를 낸 날개가 가져갑니다. 아무도 가져가지 않으면 껍데기가 벗겨집니다.

순서가 결과를 가르는 칸은 다음과 같습니다.

어떻게 갈립니까
inline.claims · block.claims처음 주장한 날개가 그 엘리먼트를 가져갑니다
keys처음 소비한 날개가 그 키를 가져갑니다
onClick · pasteText · pasteHtml · handleFiles처음 true(또는 값)를 낸 날개가 가져갑니다
fromSourceHtml처음 자기 것이라고 답한 규격이 고칩니다
툴바 기본 배치갈래별 그룹 안에서 등록 순서대로 섭니다

같은 태그를 둘이 소유(claim)하지 않습니다

원칙은 한 정규 태그(canonical tag)에 한 소유자입니다. 글머리 목록(ul)과 체크리스트 (ul[data-nabi-list="task"])처럼 속성으로 갈리는 공유만 정당하고, 그때도 두 조건을 지켜야 합니다.


채울 수 있는 칸 전부

NabiWing 의 필드는 이것이 전부입니다. 필수는 idnames 이고, 나머지는 필요할 때 채웁니다.

필드하는 일어디서 다룹니까
id전역에서 유일한 식별자이 문서
names로케일별 표시 이름이 문서
icon툴바 버튼에 그릴 것이 문서
group툴바 노출 갈래이 문서
family툴바에서 한 무리로 붙어 서는 이름이 문서
styles이 날개가 실어 나르는 CSS이 문서
inline글자에 씌우는 마크(mark)인라인 마크
block · blocks문단 자리를 차지하는 덩어리 — 블록(block)블록과 블록 속성
blockAttribute · blockAttributes태그는 그대로, 블록의 성질만 — 블록 속성(block attribute)블록과 블록 속성
picker크기 선택 격자를 먼저 띄웁니다UI 와 동작
prompt한 줄 입력창을 먼저 띄웁니다UI 와 동작
palette색 견본판(palette)을 먼저 띄웁니다UI 와 동작
activate완전 커스텀 동작UI 와 동작
commands상황 줄(context row)에 뜨는 추가 동작UI 와 동작
onClick본문 클릭을 가로챕니다UI 와 동작
keys키 하나를 가로챕니다키·자동 변환·붙여넣기
inputRules글자만으로 일어나는 자동 변환(input rule)키·자동 변환·붙여넣기
pasteText붙여넣은 평문 한 덩어리키·자동 변환·붙여넣기
pasteHtml붙여넣은 HTML 조각 통째로키·자동 변환·붙여넣기
handleFiles들어온 파일을 가져갑니다키·자동 변환·붙여넣기
prepareCompositionIME 조합 직전 캐럿(caret) 자리 준비키·자동 변환·붙여넣기
setup선언으로 안 되는 모든 것setup 과 꽃
viewer에디터 밖에서, 저장된 HTML 을 그리는 쪽에서 도는 동작setup 과 꽃

단수와 복수는 같은 관계입니다 — blockblocks: [spec] 의 편의 문법이고, blockAttributeblockAttributes: [spec] 의 편의 문법입니다. 함께 쓰면 단수가 먼저 스캔됩니다.


id

전역에서 유일한 절대 식별자입니다.

ts
readonly id: string

호스트가 코드에 적는 열쇠이기도 합니다 — editor.toggle('text-bold'), editor.runCommand('table-row-insert-above'), toolbar: [['text-bold', 'text-italic']] 이 전부 이 값을 씁니다.

  • 소문자와 하이픈만 씁니다. text-bold · block-heading-1 · table-row-insert-above.
  • 관례는 갈래를 앞에 두는 것입니다. 마크는 text-, 블록은 block-, 정렬 가족은 align-.
  • commands 안의 커맨드 id 도 같은 규칙이고, 날개 id 를 접두사로 두기를 권합니다 (link-remove · code-language · details-toggle).

한번 나가면 바꾸기 어렵습니다

id 는 호스트의 툴바 레이아웃과 저장된 설정에 박힙니다. 이름이 마음에 들지 않아 나중에 고치면 그 호스트의 툴바에서 버튼이 조용히 사라집니다 — 모르는 id 는 건너뛰기 때문입니다.


names

툴바 버튼의 tooltip, @ 메뉴 항목, 검색어로 쓰이는 로케일별 표시 이름입니다.

ts
interface WingNames {
  readonly ko: string
  readonly en: string
  readonly [locale: string]: string   // 언어를 더 채워도 됩니다
}
ts
names: { ko: '굵게', en: 'Bold' }
names: { ko: '위에 행 추가', en: 'Insert row above' }

고르는 규칙은 정확히 일치 → 언어 코드만 → en → 남은 것 중 첫 번째 입니다. locale: 'ko-KR' 이면 ko 가 걸리고, 아무 언어도 맞지 않으면 en 으로 떨어집니다.

  • koen 은 반드시 채웁니다. 나머지 언어는 선택입니다.
  • names.en 은 대문자로 시작합니다. 툴바와 @ 메뉴에 여러 날개가 나란히 서기 때문에 대소문자가 섞이면 그 자리에서 티가 납니다.
  • @ 메뉴 검색은 지금 로케일 이름 · 다른 로케일 이름 · id 를 모두 봅니다. 그래서 한국어 화면에서도 bold 로 찾을 수 있습니다.
  • 업로드 오류 문구처럼 값이 끼어드는 문장은 {param} 자리표시자(placeholder)를 씁니다 — 꽃의 t() 가 채웁니다.

icon

툴바 버튼에 그릴 것입니다. 아이콘은 주소입니다.

ts
readonly icon?: string

받는 값은 세 갈래이고, 코어가 알아서 가릅니다.

어떻게 그려집니까
주소 (data: · https: · /… · ./…)mask-image 로 얹고 바탕을 currentColor 로 칠합니다
< 로 시작하는 마크업인라인 SVG 로 심습니다 (서드파티 호환용 옛 경로)
그 밖의 짧은 문자열글자 라벨로 씁니다 ('H1')
없음표시 이름의 첫 글자를 씁니다

주소로 굳히는 도구가 셋 있습니다.

ts
import { svgIcon, markIcon, blockIcon } from 'nabi-note'

markIcon('<path d="M2 8h12"/>')   // 획 1.6 — 인라인 마크·정렬·리스트 계열
blockIcon('<rect x="1.75" y="2.75" width="12.5" height="10.5" rx="1.5"/>')  // 획 1.4 — 블록 계열
svgIcon('<path d="…"/>', 2)        // 획 굵기를 직접 정할 때
  • 몸통(<svg> 껍데기·viewBox·선 색)은 도구가 씌웁니다. 선과 도형만 넘기세요. 격자는 0 0 16 16 입니다.
  • 프리셋이 둘인 이유는 무게를 맞추기 위해서입니다. 획이 많은 블록 아이콘은 가늘게(1.4), 획이 적은 마크 아이콘은 조금 굵게(1.6) 그려야 나란히 놓았을 때 같아 보입니다.
  • 속을 채우는 도형은 불투명색으로 칠합니다. 마스크는 색이 아니라 alpha 만 보기 때문에, 칠하지 않은 도형은 뚫린 자리가 됩니다. 유튜브의 재생 삼각형이 그래서 fill="#000" 입니다.
  • 색은 넣지 마세요. currentColor 를 따라가야 눌림·비활성·다크 테마가 그대로 따라옵니다.
  • 글자 라벨이 나은 자리도 있습니다 — 제목은 icon: 'H1' 입니다.

사용자 값을 아이콘에 넣지 마세요

icon 의 SVG 문자열은 날개 작성자가 쓴 코드로 취급됩니다. 사용자에게서 받은 값을 여기에 흘려 넣으면 그대로 심어집니다.


group

툴바가 지금 상황에 맞는 버튼만 보여줄 때 쓰는 갈래입니다.

ts
type WingGroup = 'mark' | 'block' | 'insert'   // 없으면 'insert'
갈래
mark글자에 씌우는 서식굵게 · 링크 · 서식 지우기
block지금 블록의 전환·성질제목 · 리스트 · 정렬 · 코드 · 드롭 캡
insert새 덩어리를 넣습니다이미지 · 표 · 유튜브 · 구분선 · 업로드

어느 갈래가 보이는지는 캐럿이 놓인 자리가 정합니다.

자리어디입니까보이는 갈래
글 쓰는 중문단 · 제목 · 리스트 항목mark · block · insert
표 칸 안td · thmark 뿐 — 칸에는 인라인 내용만 들어갑니다
코드 블록 안pre없음 — 코드는 글자 그대로여야 합니다
물건이 통째로 선택됨이미지 · 구분선없음 — 이미지에 굵게는 뜻이 없습니다

@ 메뉴도 같은 표를 보므로, 툴바에는 없는데 메뉴로는 넣어지는 구멍이 생기지 않습니다.

  • 갈래를 적지 않으면 insert 입니다 — 어디서나 보이는 쪽이 안전합니다.
  • 이 값은 노출만 정합니다. 눌렀을 때 실제로 되는지는 각자의 조건이 따로 봅니다 (드롭 캡의 appliesTo, 커맨드의 isAvailable).

family

툴바에서 한 무리로 붙어 설 이름입니다.

ts
readonly family?: string   // 없으면 갈래(group) 전체가 한 무리

group 은 "언제 보이나" 를 가르는 값이라 셋뿐이어서, 그대로 무리로 쓰면 한 무리가 열 개를 넘어 어디서 뜻이 갈리는지 보이지 않습니다. family 는 눈이 읽는 단위입니다 — 제목 여섯, 정렬 셋, 리스트 셋처럼 같은 자리를 두고 다투는 것들이 한 이름을 나눠 갖습니다.

ts
{ id: 'text-highlight', group: 'mark', family: 'color', /* … */ }
{ id: 'text-color',     group: 'mark', family: 'color', /* … */ }

기본 날개가 쓰는 이름은 이렇습니다.

이름누가 씁니까
emphasis굵게 · 기울임 · 밑줄 · 취소선
script윗첨자 · 아랫첨자
link링크
color형광펜 · 글자색
heading제목 여섯
align왼쪽 · 가운데 · 오른쪽 정렬
typeface세리프 · 산세리프 · 고정폭
text글자 크기 · 드롭 캡
list글머리 목록 · 번호 목록 · 체크리스트
structure표 · 구분선
container코드 · 접기 상자
media이미지 · 유튜브 · 업로드
clear서식 지우기
  • 같은 이름은 등록 순서와 무관하게 모입니다. 사이에 다른 날개가 끼어도 무리는 흩어지지 않습니다.
  • 무리끼리의 순서는 그 이름이 처음 나온 자리를 따릅니다.
  • 적지 않으면 갈래 전체가 한 무리입니다. 이 칸을 모르는 서드파티 날개도 그대로 동작합니다.

styles

이 날개가 실어 나르는 스타일시트입니다. 문자열 하나입니다.

ts
readonly styles?: string
ts
import { NOTE_SCOPE } from 'nabi-note'

const DIVIDER_STYLES = `
${NOTE_SCOPE} > hr {
  margin: 1.1em 0;
  border: none;
  border-top: 1px solid var(--nabi-border);
}
`

export function horizontalRule(): NabiWing {
  return { id: 'block-divider', styles: DIVIDER_STYLES, /* … */ }
}

코어 시트(stylesheet)에는 날개가 하나도 없어도 필요한 공용 원시(틀·색 토큰·버튼·떠 있는 상자)만 있고, 서식별 규칙은 각 날개가 나릅니다. 그래서 제목을 등록하지 않은 에디터에는 제목 CSS 가 아예 들어가지 않습니다.

  • mount 할 때 코어 시트 뒤에 주입되므로, 같은 우선순위에서는 날개가 이깁니다.
  • 내용이 같은 문자열은 문서당 한 장입니다. 한 가족 날개들이 같은 상수를 나눠 가지면 사본이 쌓이지 않습니다 — heading(1)~heading(6)HEADING_STYLES 하나를, 윗첨자·아랫첨자가 SUBSUP_STYLES 하나를, 세 정렬이 ALIGN_STYLES 하나를 씁니다.
  • 마지막 에디터가 destroy() 될 때 걷힙니다.
  • 뿌리는 물음 하나로 고릅니다 — 이 규칙이 getHtml() 결과에도 걸려야 하나요? 걸려야 하면 NOTE_SCOPE(문서 서식 — 저장된 HTML 의 모양), 아니면 EDITOR_SCOPE(편집 화면 전용 — 캐럿 표시·임시 마크업)를 앞에 세웁니다. wing 이 스스로 그리는 상자는 자기 블록 클래스입니다 — .nabi-<이름>, 부품은 .nabi-<이름>__<부품>(kebab-case), 상태는 .nabi-<이름>--<상태>.
  • 자기 색 토큰을 선언한다면 TOKEN_SCOPE 에 선언하세요 — 아니면 에디터 밖에서 그린 저장값에서 var(--nabi-…) 가 값을 얻지 못합니다.
  • 색·모서리·그림자는 var(--nabi-*) 토큰으로 씁니다. 자기 색 토큰이 필요하면 라이트·다크· 명시적 라이트 세 블록을 스스로 재현해야 합니다 — 하나라도 빠지면 그 테마에서 색이 샙니다.
  • 문자열 안에 백틱을 두지 마세요.

날개 시트는 문서 전역입니다

그래서 "이 에디터만 세 줄, 저 에디터는 다섯 줄" 같은 값을 날개 옵션으로 두면 안 됩니다 — 같은 문서에 에디터가 둘 있으면 나중 시트가 둘 다 이기고 조용히 어긋납니다. 자리마다 달라져야 하는 값은 CSS 변수로 열어 두세요. 드롭 캡이 감쌀 줄 수를 --nabi-dropcap-lines 로 받는 것이 그 이유입니다.

브라우저 기본값에 기대지 마세요. 호스트가 리셋 CSS(예: ol, ul { list-style: none })를 쓰면 목록의 점과 번호가 통째로 사라집니다. 시트를 나르는 날개가 자기 마크업의 모양을 책임진다는 것이 규칙이라, 리스트 날개는 표식을 스스로 선언합니다.


다음 문서