# npm — `@minjun0219/mdwire`

> @minjun0219/mdwire 패키지의 render, renderWithReport, Streamer, 그리고 React · events 진입점을 설명합니다.

HTML: https://minjun.kim/mdwire/ko/docs/npm/

Rust 코어를 WASM 으로 컴파일한 패키지입니다. React 와 이벤트 목록용 진입점은 JS 로 직접 작성했습니다.
Node, Bun, 번들러에서 동작합니다.

```sh
npm install @minjun0219/mdwire
```

패키지는 `exports` 조건으로 빌드를 고릅니다. Node 에서는 WASM 을 디스크에서 읽는 CommonJS 빌드를,
그 밖의 환경에서는 ESM 번들러 빌드를 씁니다. **번들러에서는** WASM 을 ES 모듈로 import 하므로, Vite 를
쓴다면 `vite-plugin-wasm` 과 `build.target: "esnext"`(top-level `await` 용)가 필요합니다.
[`examples/react-streaming/vite.config.js`](https://github.com/minjun0219/mdwire/blob/main/examples/react-streaming/vite.config.js) 를 참고하세요.

채널 이름은 [Concepts](https://minjun.kim/mdwire/ko/docs/)에 나온 문자열을 씁니다.

## `render(input, channel, options?)`

```ts
import { render } from "@minjun0219/mdwire";

const parts: string[] = render(markdown, "telegram-html");
```

완성된 문서를 변환해 보낼 조각 배열을 반환합니다. 채널의 길이 제한(또는 `options.limit`)을 넘지 않으면
조각은 하나입니다.

## `renderWithReport(input, channel, options?)`

```ts
import { renderWithReport } from "@minjun0219/mdwire";

const out = renderWithReport(markdown, "slack-markdown");
out.parts;                   // string[]
out.repairs.closedEmphasis;  // 아래 "Repairs" 참고
out.free();                  // 결과는 WASM 메모리에 있음
```

`render` 에 [정규화 보고](https://minjun.kim/mdwire/ko/docs/#%EC%A0%95%EA%B7%9C%ED%99%94-%EB%B3%B4%EA%B3%A0)를 더한 함수입니다. 결과와 그 `repairs` 는 WASM 객체이므로,
다 쓰면 `free()` 로 해제하거나 `using`(`Symbol.dispose`)으로 감싸 쓰세요.

## `limit(channel)`

채널의 조각 길이 제한을 숫자로 반환합니다. `html` 은 제한이 없어서 `4294967295` 를 반환합니다.

## `new Streamer(channel, options?)`

```ts
import { Streamer } from "@minjun0219/mdwire";

const s = new Streamer("telegram-html");
let acc = "";
for await (const token of tokens) {
  acc += s.push(token);           // 확정된 출력. 다시 바뀌지 않음
  await edit(acc + s.preview());  // + 보류 중인 내용을 입력이 끝난 것처럼 렌더링한 뒷부분
}
acc += s.finish();
if (s.revised()) await edit(acc); // 마지막 화면이 완성본과 같으면 건너뜀
s.free();
```

| 메서드 | 반환 | |
|---|---|---|
| `push(chunk)` | `string` | 지금 보내도 안전한 출력입니다. 이 출력은 확정입니다. |
| `preview()` | `string` | 메시지 전체를 다시 렌더링할 때 붙이는 뒷부분입니다. 상태를 바꾸지 않습니다. 청크마다 부르지 말고 화면을 갱신할 때만 호출하세요. |
| `closeOpen()` | `string` | 이미 보낸 블록 마크업만 닫는 뒷부분입니다. 상태를 바꾸지 않습니다. |
| `finish()` | `string` | 남은 출력입니다. 열린 마크업은 닫아서 반환합니다. |
| `revised()` | `boolean` | `finish` 다음에 호출합니다. 완성본이 마지막 `preview()` 화면과 다른지 알려 줍니다. |
| `repairs()` | `Repairs` | 지금까지 센 정규화 보고입니다. `finish` 다음에는 문서 전체에 대한 값입니다. |
| `free()` | | WASM 메모리를 해제합니다. |

덧붙이기만 하는 채널(슬랙 `appendStream`)에는 `push` 결과를 그대로 보내고 `preview()` 는 호출하지
않습니다. [스트리밍 계약](https://minjun.kim/mdwire/ko/docs/#%EC%8A%A4%ED%8A%B8%EB%A6%AC%EB%B0%8D-%EA%B3%84%EC%95%BD)을 참고하세요.

## `RenderOptions`

```ts
interface RenderOptions {
  limit?: number;                       // 1 이상의 정수. 256 보다 작으면 256 으로 올림
  html?: {                              // "html" 채널에서만
    lineBreaks?: "br" | "space";        // 기본 "br"
    images?: "link" | "load";           // 기본 "link"
    schemes?: string[];                 // 기본 ["http", "https", "mailto"]. 더하지 않고 바꿈
  };
}
```

모르는 채널 이름, 모르는 `html` 값, 1 이상의 정수가 아닌 `limit` 을 주면 `Error` 를 던집니다.

## `Repairs`

| 필드 | |
|---|---|
| `closedEmphasis` · `closedFence` · `revertedCodeSpan` · `droppedMarker` · `guessedPair` (다음 릴리스) | 고친 횟수입니다. |
| `escapedChar` · `tagEmphasis` · `strippedHtml` · `rewrittenBullet` · `rewrittenTable` · `convertedMarker` | 채널에 맞춰 바꾼 횟수입니다. |

각 필드가 세는 것은 [Concepts](https://minjun.kim/mdwire/ko/docs/#%EC%A0%95%EA%B7%9C%ED%99%94-%EB%B3%B4%EA%B3%A0)에 있습니다.

## React — `@minjun0219/mdwire/react`

React 요소를 `createElement` 로 만들며 `innerHTML` 을 쓰지 않습니다. 이스케이프, 허용 태그, 링크 스킴은
코어의 `html` 채널이 정하고, 사용하는 쪽은 태그마다 어떤 컴포넌트로 렌더링할지만 고릅니다.

```tsx
import { Markdown, useMarkdownStream } from "@minjun0219/mdwire/react";

<Markdown text={answer} components={{ a: RouterLink }} />

const { elements, push, finish } = useMarkdownStream();      // push(token) … finish()
const settled = useMarkdownStream({ eager: false });        // 확정된 출력만
```

| | |
|---|---|
| `<Markdown text components? options? />` | 완성된 답변을 렌더링합니다. `components` 로 태그(`a`, `code` …)를 직접 만든 컴포넌트에 연결합니다. |
| `useMarkdownStream({ components?, options?, eager?, onSettled? })` | 스트리밍용 훅입니다. `{ elements, push, finish }` 를 반환합니다. `eager`(기본 `true`)면 보류 중인 내용도 먼저 렌더링하고, `false` 면 확정된 출력만 렌더링합니다. `onSettled(html, revised)` 는 `finish` 다음에 호출됩니다. 옵션은 `onSettled` 를 빼고 처음 한 번만 읽습니다. |
| `toElements(html, components?, schemes?)` | `html` 채널 출력(스트리밍이라면 누적 출력 + `preview()`)을 React 노드로 바꿉니다. `schemes` 는 코어에 준 값과 같게 주세요. |

## 이벤트 — `@minjun0219/mdwire/events`

```ts
import { toEvents } from "@minjun0219/mdwire/events";

toEvents(html); // [{ type: "open", tag: "p", attrs: {} }, { type: "text", text: "…" }, { type: "close", tag: "p" }, …]
```

같은 `html` 출력을 `open` / `text` / `close` / `void` 이벤트가 중첩 없이 이어진 목록으로 바꿉니다. React 가 아닌
프레임워크에서 쓰는 진입점입니다. 속성은 코어가 출력하는 것만 담깁니다. `a` 의 `href`, `code` 의
`class`(`language-…`), `th` · `td` 의 `style`(`text-align:…`), `ol` 의 `start`, 이미지를 불러올 때
`img` 의 `src` · `alt` 입니다.

## lezer — `@minjun0219/mdwire/lezer` (다음 릴리스)

```ts
import { GFM, parser } from "@lezer/markdown";
import { koreanEmphasis } from "@minjun0219/mdwire/lezer";

const p = parser.configure([GFM, koreanEmphasis]); // GFM 뒤에 둡니다 — Emphasis · Strikethrough 파서를 같은 이름으로 바꿔 끼웁니다
// CodeMirror: markdown({ base: markdownLanguage, extensions: [koreanEmphasis] })
```

mdwire 를 거치지 않고 `@lezer/markdown` 이나 CodeMirror 로 마크다운을 그리는 화면을 위한 확장입니다. 코어와
[같은 규칙](https://minjun.kim/mdwire/ko/docs/emphasis/)으로 강조를 읽어서 `**설정(config)**을` 이 별표 대신 굵게로 보입니다. WASM 이 없고,
`@lezer/markdown`(1.5 이상)은 선택 peer 의존성입니다.

코어와 다른 점은 셋이고 모두 의도한 것입니다. 짝을 맺지 못한 마커는 글자로 남깁니다(코어는 일부를 버립니다).
문단 끝까지 열려 있는 강조도 글자로 남깁니다(코어는 닫아 줍니다). 별표 넷 이상은 글자입니다. 실제 문단 6,965개로
코어 출력과 대조했습니다.
