persona wysiwym markdown editor
  • JavaScript 86.3%
  • CSS 7.9%
  • HTML 5.8%
Find a file
2026-10-13 10:05:45 +09:00
.idea 리팩터링 1단계: 연속 단락 간 빈 줄 제거 및 툴바 버튼 클릭 시 포커스 유지 기능 추가 2026-10-13 10:05:45 +09:00
scripts 리팩터링 2단계 2026-09-22 10:35:24 +09:00
src 리팩터링 1단계: 연속 단락 간 빈 줄 제거 및 툴바 버튼 클릭 시 포커스 유지 기능 추가 2026-10-13 10:05:45 +09:00
test 리팩터링 1단계: 연속 단락 간 빈 줄 제거 및 툴바 버튼 클릭 시 포커스 유지 기능 추가 2026-10-13 10:05:45 +09:00
.gitignore refactoring 2026-09-01 22:00:33 +09:00
AGENTS.md 리팩터링 1단계: 연속 단락 간 빈 줄 제거 및 툴바 버튼 클릭 시 포커스 유지 기능 추가 2026-10-13 10:05:45 +09:00
index.html 코드 정리 2026-09-18 15:16:01 +09:00
LICENSE refactoring 2026-09-02 00:05:49 +09:00
package.json 리팩터링 2단계 2026-09-22 10:35:24 +09:00
README.md 리팩터링 1단계: 연속 단락 간 빈 줄 제거 및 툴바 버튼 클릭 시 포커스 유지 기능 추가 2026-10-13 10:05:45 +09:00

Persona Editor

Vanilla JavaScript, Web Components, ProseMirror로 만든 Typora 스타일 WYSIWYM Markdown 에디터입니다. 런타임 문서의 권한은 ProseMirror Document가 가지며, Markdown은 로드·저장·소스 모드 전환 때 변환됩니다. 모든 모듈에는 누구나 쉽게 이해하고 유지보수할 수 있도록 상세한 JSDoc 주석이 작성되어 있습니다.

현재 상태

마크다운 편집 기능과 확장 문법 단계가 구현되어 있습니다.

1단계 안정화 작업을 반영했습니다. 인라인 수식의 TeX caret 보존, 연속 단락 간 single newline 직렬화(Enter 입력 시 두 줄 사이 빈 줄 제거), 블록 수식 경계의 불필요한 빈 줄 제거, 정의 목록 Enter 전환, HTML mark delimiter decoration, Shadow DOM 테마 변수 연결, heading focus outline 제거, 툴바 버튼 클릭 시 포커스 유지를 지원합니다.

  • Web Component: <persona-editor>
  • Heading 1~6, paragraph, blockquote, bullet/ordered list, code block, horizontal rule
  • Bold, italic, inline code, link, strikethrough
  • Markdown input rules와 undo/redo
  • 헤딩 및 리스트 블록 시작 태그(#, - , 1. , - [ ] )의 Method B 보존 및 포커스 동적 편집 지원 (포커스 시 태그 직접 수정/전환/삭제 가능)
  • 인용 블럭 포커스 시 Markdown 시작 태그(>)를 편집 상태로 표시
  • GFM table, task list, image 삽입/드래그 앤 드롭/리사이즈
  • 커서 위치에 따른 syntax delimiter 숨김
  • 렌더링 모드와 소스 모드 전환 (Ctrl+/, Cmd+/) 및 소스 모드 스크롤 연동
  • 소스 모드 진입 시 현재 ProseMirror Document를 다시 직렬화하여 수식의 ^와 TeX 원문 보존
  • 수식 렌더링 시 zero-width Unicode 문자를 정리해 KaTeX 경고를 방지하면서 원문은 보존
  • 라이트/다크 테마 및 검색 범위 강조 API (highlight(from, to))
  • inline $...$ 및 block $$...$$ 수식 문법 보존/표시 (KaTeX 번들)
  • 인라인/블록 수식은 해당 수식에 포커스가 있을 때만 원문을 편집하고, 포커스를 잃으면 KaTeX preview로 자동 전환
  • markdown-it 확장 플러그인 연동: abbreviation, anchor, deflist, emoji, ins, mark, sub, sup, table, task-list
  • GFM 자동 링크, ==강조==, ++삽입++, H~2~O, x^2^ 등 인라인 확장 문법
  • 중첩 인라인 마크 지원: ==**Text**== → <mark><strong>Text</strong></mark> 처럼 wrapper mark 내부에 strong/em 등 중첩 렌더링
  • 인라인 코드 내 마크다운 차단: `**text**` 같은 코드 범위 내부에는 다른 mark/decoration이 적용되지 않음
  • 이모지 렌더링: :smile: 같은 이모지 단축 코드를 실제 유니코드 이모지(😄)로 렌더링
  • 선택적 백엔드 Markdown 파서 연동(로컬 파서는 오프라인 fallback)
  • 문법 registry(syntax-registry.js)를 inline parser, Method B normalize, delimiter decoration, block marker 처리 및 serializer가 공유하여 문법별 정규식·우선순위·delimiter 정책을 단일화
  • 문서 변환 도메인은 src/document/로 분리되어 parser, serializer, block/inline parser, token utility, 이미지·HTML sanitizer를 독립적으로 관리하며, 기존 src/editor/parser.js·serializer.js 등은 호환 facade로 유지
  • NodeView는 src/editor/node-views/<feature>/ 단위로 분리되며, view·DOM helper·command 모듈이 package entry point를 통해 연결되고 기존 createNodeViews() registry API를 유지
  • 표 행·열 추가/삭제, 셀 정렬 컨트롤 및 스키마 기반 안전한 블록 분할 삽입
  • 이미지 붙여넣기, 드래그앤드롭, 키보드 리사이즈
  • 모바일 toolbar 레이아웃과 기본 ARIA 라벨
  • 전 모듈(17개 파일)에 대한 100% 상세 JSDoc 및 인라인 주석 보강
  • 예시 페이지에서 기본 Markdown, GFM, 확장 문법을 직접 편집하고 확인할 수 있음

코드 블록은 highlight.js overlay를 사용해 편집 가능한 contentDOM을 유지하면서 syntax highlighting을 제공합니다. KaTeX는 로컬 번들로 inline/block 수식을 렌더링하며, Shadow DOM 내부에서도 동작하도록 bundle-text:katex/dist/katex.min.css를 컴포넌트 스타일에 포함합니다. Mermaid는 globalThis.mermaid가 연결된 경우 strict-security SVG preview를 표시하며 연결되지 않은 환경에서는 안전한 원문 fallback을 표시합니다.

설치 및 실행

yarn install
yarn dev

개발 서버는 보통 http://localhost:1234에서 실행됩니다.

yarn build

프로덕션 번들은 dist/에 생성됩니다.

테스트

현재 test/editor.test.js는 다음 핵심 동작을 Node.js 기본 테스트 러너로 검증합니다.

  • Markdown → ProseMirror Document → Markdown round-trip 및 직렬화 안정성
  • 중첩 인라인 마크와 인라인 코드 내부 마크 차단
  • Method B normalize 및 syntax decoration
  • 서식 토글, 헤딩 전환, 테이블·구분선 삽입 명령
  • history, input rule, normalize, keymap 플러그인 조합
npm test

실제 브라우저의 Web Component 생명주기, 키 입력, Shadow DOM, source mode, HTML delimiter, 수식·표 NodeView는 Chrome DevTools Protocol 기반 회귀 테스트로 검증할 수 있습니다. Chrome/Chromium이 설치되어 있어야 합니다.

yarn test:browser

CI에서 브라우저 경로가 다르면 CHROME_BIN 환경 변수로 실행 파일을 지정합니다.

브라우저에 의존하는 Web Component 생명주기, 실제 키 입력, Shadow DOM, source mode, HTML delimiter, 수식·표 NodeView 동작은 Node.js 단위 테스트와 분리된 Chrome DevTools Protocol 기반 브라우저 회귀 테스트로 검증합니다.

사용 예시

<persona-editor id="editor"></persona-editor>
<script type="module" src="./src/components/persona-editor.js"></script>
<script>
  const editor = document.querySelector('#editor');
  editor.addEventListener('change', (event) => {
    console.log(event.detail.markdown);
  });
  editor.setMarkdown('# Hello\n\n**Markdown**');
</script>

초기 문서는 요소 내부의 Markdown 텍스트로도 지정할 수 있습니다. HTML로 해석되지 않도록 JavaScript에서 setMarkdown()을 호출하는 방식을 권장합니다.

<persona-editor id="editor" backend-parser-url="/api/markdown/parse"></persona-editor>
<script type="module">
  import './src/components/persona-editor.js';

  const editor = document.querySelector('#editor');
  editor.setMarkdown(`# 문서 제목

**굵게**, ==강조==, x^2^, H~2~O, :smile:

- [x] 완료`);
</script>

Public API

Mermaid preview는 번들 크기와 Parcel의 Mermaid 12 Node 의존성 문제를 피하기 위해 optional adapter로 동작합니다. Mermaid를 사용하는 호스트는 번들에서 Mermaid를 로드한 뒤 전역에 연결하면 됩니다.

import mermaid from 'mermaid';
globalThis.mermaid = mermaid;

문서·편집 API

editor.getMarkdown();                 // 현재 Markdown 반환
editor.setMarkdown(markdown);         // 문서 교체
editor.toggleSource();                // 렌더링/소스 모드 전환
editor.isSourceMode();                // 현재 소스 모드 여부
editor.focus();                       // 편집 영역에 포커스
editor.destroy();                     // ProseMirror와 리소스 정리
editor.scrollTo(3);                   // 1부터 시작하는 라인으로 이동

editor.highlight(10, 20);             // 단일 문서 위치 범위
editor.highlight([]);                 // 모든 강조 제거
editor.highlight([
  { from: 10, to: 20 },
  [35, 42],
]);

테마 API

editor.setTheme('dark');              // 'light' 또는 'dark'
editor.getTheme();
editor.toggleTheme();

백엔드 파서 API

editor.setBackendParserUrl('/api/markdown/parse');
editor.getBackendParserUrl();

try {
  await editor.parseWithBackend();
  // 또는 await editor.parseWithBackend(markdown, { signal });
} catch (error) {
  console.error(error);
}

isSourceModeActive()는 이전 호환성을 위한 deprecated alias이며 isSourceMode() 사용을 권장합니다.

내용이 변경되면 다음 이벤트가 발생합니다.

editor.addEventListener('change', (event) => {
  const markdown = event.detail.markdown;
});

editor.addEventListener('backend-error', (event) => {
  console.error('백엔드 파서 오류:', event.detail);
});

backend-parser-url 속성을 지정하지 않으면 네트워크 요청 없이 로컬 파서만 사용합니다.

아키텍처

Markdown
  ↓ parser.js
ProseMirror Document  ← 런타임 권한
  ↓ normalize.js
marks + decorations
  ↓ serializer.js
Markdown

인라인 문법은 Method B를 사용합니다. **text**, `code` 같은 delimiter는 텍스트에 보존하고, normalize 플러그인이 내부 범위에 ProseMirror mark를 파생합니다. serializer는 문서의 텍스트와 블록 구조를 다시 Markdown으로 출력합니다. 편집 중 표시되는 Markdown delimiter는 테마별 --syntax-color로 통일해 강조하며, 커서가 문법 범위를 벗어나면 색상·투명도·간격에 transition을 적용해 자연스럽게 숨깁니다.

주요 파일은 다음과 같습니다.

src/
├── components/persona-editor.js       얇은 Web Component와 Public API facade
├── components/editor-shell.js         Shadow DOM 레이아웃 및 스타일 조합
├── components/toolbar.js              toolbar DOM과 사용자 의도 전달
├── components/editor-controller.js    ProseMirror 상태·명령·백엔드 생명주기
├── components/source-mode.js           Markdown source mode 생명주기
├── components/image-handlers.js       이미지 파일·paste·drop 처리
├── components/math-renderer.js        수식 preview 동기화
├── components/theme-controller.js     테마 상태와 Shadow DOM 반영
├── editor/schema.js                   ProseMirror schema
├── editor/syntax-registry.js          inline 문법 정규식·delimiter 정책의 단일 출처
├── editor/parser.js                   Markdown-it 초기화 및 parser facade
├── editor/block-parser.js             블록 token → ProseMirror node
├── editor/inline-parser.js             인라인 token → ProseMirror inline node
├── editor/parser-utils.js              token 탐색 및 입력 보안 유틸리티
├── editor/serializer.js               ProseMirror → Markdown
├── editor/normalize.js                 Method B mark 파생
├── editor/decorations.js               syntax 숨김/표시
├── editor/input-rules.js               Markdown 단축키
├── editor/plugins.js                   플러그인 조합
└── editor/node-views/                  NodeView registry 및 기능별 패키지

PersonaEditor는 Web Component 생명주기와 public API만 담당하며, shell·toolbar·source mode·이미지 입력·수식 렌더링·테마 로직은 위의 전용 모듈에서 관리합니다.

NodeView는 node-views/{feature}/index.js를 기능별 공개 진입점으로 사용합니다. ProseMirror 설정은 node-views/index.js의 registry만 참조하므로 Controller가 개별 NodeView 구현 경로에 의존하지 않습니다. 기존 flat import 경로는 호환성을 위해 유지됩니다.

선택 기능은 src/adapters/ 경계에 격리합니다. KaTeX, highlight.js, Mermaid는 핵심 문서 모델·명령·직렬화 코드에서 직접 참조하지 않으며, 각 렌더링 경계가 adapter API를 통해서만 사용합니다. Mermaid가 globalThis.mermaid에 연결되지 않은 환경에서는 안전한 원문 fallback을 표시합니다.

단축키

단축키 기능
Ctrl+/ / Cmd+/ 소스 모드 전환
Ctrl+B / Cmd+B 굵게
Ctrl+I / Cmd+I 기울임
Ctrl+K / Cmd+K 링크
Ctrl+Z / Cmd+Z 실행 취소
# , > , - , 1. , ````` Input rule
- [ ] , - [x] 체크리스트
Tab / Shift+Tab 목록 들여쓰기·내어쓰기 또는 표 셀 이동
Backspace (블록 시작) 헤딩 해제 또는 목록 항목 내어쓰기

백엔드 파서 연동

백엔드 연동은 기본 동작이 아니며 backend-parser-url 속성 또는 setBackendParserUrl()로 활성화합니다. 서버에는 다음 요청이 전송됩니다.

POST /api/markdown/parse
Content-Type: application/json

{"markdown":"# 제목","format":"markdown","version":1}

응답은 정규화된 Markdown을 반환하는 { "markdown": "..." } 형식이나, 동일한 스키마의 ProseMirror JSON을 반환하는 { "document": { ... } } 형식을 사용할 수 있습니다.

<persona-editor id="editor" backend-parser-url="/api/markdown/parse"></persona-editor>
<script>
  await editor.parseWithBackend('==서버에서 검증할 문서==');
  editor.addEventListener('backend-error', (event) => console.error(event.detail));
</script>

Java/Spring 등 서버 구현은 이 HTTP 계약만 지키면 되며, URL이 없거나 서버가 unavailable한 경우에도 에디터의 로컬 파서는 계속 사용할 수 있습니다.

기술 원칙

  • React, Vue, Svelte 없이 Native Web Components만 사용합니다.
  • 기본 편집 경로는 네트워크 없이 동작해야 합니다.
  • HTML을 표시하는 기능을 추가할 때는 신뢰 경계와 sanitization 정책을 먼저 정합니다.
  • 기능을 완료로 표시하기 전에 parser → document → serializer와 실제 사용자 입력을 함께 검증합니다.

라이선스

Apache-2.0