- JavaScript 86.3%
- CSS 7.9%
- HTML 5.8%
| .idea | ||
| scripts | ||
| src | ||
| test | ||
| .gitignore | ||
| AGENTS.md | ||
| index.html | ||
| LICENSE | ||
| package.json | ||
| README.md | ||
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