@minjun0219/mdwire 0.1.7 → 0.1.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -3,14 +3,16 @@
3
3
  English | [한국어](README.ko.md)
4
4
 
5
5
  Send LLM-generated Markdown to chat channels without it breaking.
6
+ Docs, API reference and a live demo: [mdwire.minjun.dev](https://mdwire.minjun.dev).
6
7
 
7
8
  Agents emit Markdown. Chat channels don't accept it — each has its own subset, its own
8
9
  escaping rules, and its own length limit. Existing converters assume the input is
9
10
  well-formed CommonMark and target one channel at a time. Neither assumption holds for
10
11
  agent output.
11
12
 
12
- **Status: v0.1.7.** Normalizing, rendering, splitting and streaming work for three
13
- channels — Telegram HTML, Slack `markdown_text`, and plain text — from a Rust core, a CLI,
13
+ **Status: v0.1.9.** Normalizing, rendering, splitting and streaming work for six
14
+ targets — Telegram HTML, Slack `markdown_text`, GitHub comments (GFM), Notion pages, plain text,
15
+ and HTML for the browser — from a Rust core, a CLI,
14
16
  an npm package (WASM), and a Go port. See `SPEC.md` for what is in v0.1 and what was
15
17
  deliberately deferred. `SPEC.md` and `DESIGN.md` are written in Korean.
16
18
 
@@ -23,24 +25,37 @@ LLM markdown → normalize → render for channel → split safely → s
23
25
  1. **Normalize.** Agent output is not well-formed. Unpaired `**`, emphasis that spans a
24
26
  line break in wrapped prose, unclosed code fences. Repair before rendering.
25
27
  2. **Render.** Emit the syntax the channel actually accepts. Telegram HTML allows nine
26
- tags; Slack `markdown_text` takes standard Markdown directly.
28
+ tags; Slack `markdown_text` takes standard Markdown directly. GitHub takes it too, but
29
+ reads a lone `~` as strikethrough and `<T>` as an HTML tag — so a `~` or `<` meant as a
30
+ character goes out escaped (`\~`, `\<`), and emphasis GFM would not close — `**(a)**`
31
+ followed directly by a Korean particle — goes out as `<strong>`. Notion draws that bold as is
32
+ but shows inline HTML as text, so `notion-markdown` strips the tags and escapes a literal `*`
33
+ or `\` instead. For the browser, `html` draws
34
+ blocks as tags too and is safe to set as `innerHTML`: text is escaped, inline tags from the
35
+ source keep no attributes, and only `http(s)`/`mailto` links become `<a>` — line breaks,
36
+ images and allowed schemes are options. While streaming,
37
+ the accumulated output plus `preview()` (or `closeOpen()`) is always balanced HTML.
27
38
  3. **Split.** Respect the channel's limit — and never cut through markup. This also
28
39
  covers streaming: a chunk boundary must not land inside `**bold**`.
29
40
 
30
41
  Two options around that pipeline. **Input dialect:** an agent that learned Slack from its
31
42
  docs writes legacy `mrkdwn` (`*bold*`, `~strike~`, `<url|text>`); `--from slack-mrkdwn`
32
- reads it as such instead of as standard Markdown. **Repair report:** how many times the
43
+ reads it as such instead of as standard Markdown. **Change report:** how many times the
33
44
  normalizer stepped in — unclosed emphasis, unclosed fence, unpaired backticks, dropped
34
- markers — so you can log how often the model breaks its own formatting.
45
+ markers — and what it rewrote for the channel — escaped characters, tags for emphasis,
46
+ stripped HTML, bullets, tables, converted markers — so you can log how often the model
47
+ breaks its own formatting and see what a channel changes before you adopt it.
35
48
 
36
49
  ## Use it
37
50
 
38
51
  ```sh
39
52
  cat agent-output.md | mdwire --channel telegram-html # parts separated by NUL
40
53
  cat agent-output.md | mdwire --channel slack-markdown --stream # emit as it arrives
54
+ cat agent-output.md | mdwire --channel plain --limit 4096 # plain fallback into Telegram
41
55
 
42
56
  # The agent wrote Slack's legacy mrkdwn (*bold*, ~strike~)? Say so. --report prints what
43
- # the normalizer fixed (unclosed emphasis, unclosed fence, …) as one JSON line on stderr.
57
+ # the normalizer fixed and rewrote (unclosed emphasis, escaped `~`, stripped tags, …) as one
58
+ # JSON line on stderr.
44
59
  cat agent-output.md | mdwire --channel slack-markdown --from slack-mrkdwn --report
45
60
  ```
46
61
 
@@ -58,17 +73,19 @@ import { render, renderWithReport, Streamer } from "@minjun0219/mdwire"; // np
58
73
  const parts = render(markdown, "telegram-html");
59
74
  const { repairs } = renderWithReport(markdown, "slack-markdown", { from: "slack-mrkdwn" });
60
75
 
61
- // A channel that rewrites the whole message (Telegram edit): send acc plus the tail
62
- // that closes open blocks. Keep acc itself untouched.
76
+ // A channel that rewrites the whole message (Telegram edit): send acc plus the preview —
77
+ // what is still held (open bold, table rows, a code span) drawn as if the input ended here.
78
+ // Keep acc itself untouched. After finish, skip the last edit if nothing changed.
63
79
  const s = new Streamer("telegram-html");
64
80
  let acc = "";
65
81
  for await (const chunk of tokens) {
66
82
  acc += s.push(chunk);
67
- await edit(acc + s.closeOpen());
83
+ await edit(acc + s.preview());
68
84
  }
69
85
  acc += s.finish();
86
+ if (s.revised()) await edit(acc);
70
87
 
71
- // An append-only channel (Slack appendStream): send each piece as is — never closeOpen.
88
+ // An append-only channel (Slack appendStream): send each piece as is — never preview.
72
89
  const t = new Streamer("slack-markdown");
73
90
  for await (const chunk of tokens) {
74
91
  const piece = t.push(chunk);
@@ -77,12 +94,31 @@ for await (const chunk of tokens) {
77
94
  await append(t.finish());
78
95
  ```
79
96
 
97
+ In React, `@minjun0219/mdwire/react` builds elements with `createElement` — no
98
+ `innerHTML`. Escaping, the tag set and link schemes are decided once, in the core's `html`
99
+ channel; you choose which component draws each tag. `@minjun0219/mdwire/events` gives the
100
+ same output as an `open` / `text` / `close` event list for other frameworks.
101
+
102
+ ```jsx
103
+ import { Markdown, useMarkdownStream } from "@minjun0219/mdwire/react";
104
+
105
+ <Markdown text={answer} components={{ a: RouterLink }} /> // a finished answer
106
+ const { elements, push, finish } = useMarkdownStream(); // streaming: push(token), finish()
107
+ ```
108
+
109
+ The hook draws held content early by default; `useMarkdownStream({ eager: false })` shows only
110
+ what is final, and `onSettled(html, revised)` tells you whether the finished text differs from
111
+ the last frame.
112
+
113
+ [`examples/react-streaming`](examples/react-streaming) streams one answer into react-markdown, Streamdown,
114
+ mdwire in front of Streamdown, and mdwire side by side. Measured numbers are in `DESIGN.md`.
115
+
80
116
  **Append-only contract.** What `push` returns is final — a later chunk never rewrites it —
81
117
  and `finish` only appends the tail. So the pieces concatenated equal a one-shot `render`,
82
118
  whatever the chunk size (unless the document is long enough to be split into parts).
83
119
  This is tested on the corpus, by fuzzing, and by `mdwire-check --scan <dir>`, which streams
84
- every file one character and 64 characters at a time and reports any divergence. Runs in
85
- Node, Bun, and bundlers. See `SPEC.md` §8.2.
120
+ every file one character and 64 characters at a time and reports any divergence. See
121
+ `SPEC.md` §8.2.
86
122
 
87
123
  The streamer holds back only what it must: a prefix it cannot classify yet, a marker run
88
124
  at the end of a chunk, and the inside of an emphasis that has not closed. Paragraphs are
@@ -128,11 +164,11 @@ Every release also carries its own artifacts, if you would rather not go through
128
164
 
129
165
  ```sh
130
166
  # npm package (works under a bundler and in plain Node)
131
- npm install https://github.com/minjun0219/mdwire/releases/download/v0.1.7/mdwire-0.1.7.tgz
167
+ npm install https://github.com/minjun0219/mdwire/releases/download/v0.1.9/mdwire-0.1.9.tgz
132
168
 
133
169
  # CLI binary — macOS (Apple silicon) or Linux (x86_64)
134
- curl -L https://github.com/minjun0219/mdwire/releases/download/v0.1.7/mdwire-v0.1.7-aarch64-apple-darwin.tar.gz | tar xz
135
- curl -L https://github.com/minjun0219/mdwire/releases/download/v0.1.7/mdwire-v0.1.7-x86_64-unknown-linux-gnu.tar.gz | tar xz
170
+ curl -L https://github.com/minjun0219/mdwire/releases/download/v0.1.9/mdwire-v0.1.9-aarch64-apple-darwin.tar.gz | tar xz
171
+ curl -L https://github.com/minjun0219/mdwire/releases/download/v0.1.9/mdwire-v0.1.9-x86_64-unknown-linux-gnu.tar.gz | tar xz
136
172
  ```
137
173
 
138
174
  The release notes list a SHA-256 for every artifact — pin to it when installing by URL.
@@ -187,8 +223,8 @@ mdwire-check --scan ./some-directory-of-markdown # invariants only, no expecte
187
223
 
188
224
  ## Releasing
189
225
 
190
- Nobody edits the version by hand. After every merge to `main` a bot keeps a
191
- `release: X.Y.Z` pull request open; merging it tags `vX.Y.Z` and `go/vX.Y.Z` and publishes
226
+ Nobody edits the version by hand. When a merge to `main` changes what ships (the core, CLI,
227
+ WASM or Go sources, manifests, npm packaging), a bot keeps a `release: X.Y.Z` pull request open; merging it tags `vX.Y.Z` and `go/vX.Y.Z` and publishes
192
228
  the release with its artifacts. See `AGENTS.md`.
193
229
 
194
230
  ## License
@@ -5,6 +5,17 @@
5
5
  export interface RenderOptions {
6
6
  /** 입력 표기. 슬랙 레거시 mrkdwn(`*굵게*` `~취소~`)으로 쓴 에이전트 출력이면 "slack-mrkdwn". */
7
7
  from?: "markdown" | "slack-mrkdwn";
8
+ /** 조각 한도(글자 수). 생략하면 채널의 한도다 — plain 을 텔레그램에 보내면 4096. 스트리밍은 나누지 않는다. */
9
+ limit?: number;
10
+ /** 브라우저 채널("html")의 정책. 기본값이 가장 보수적이다. */
11
+ html?: {
12
+ /** 블록 안 줄바꿈. "br"(기본) 은 `<br>`, "space" 는 브라우저가 공백으로 접게 둔다. */
13
+ lineBreaks?: "br" | "space";
14
+ /** 이미지. "link"(기본) 는 누르기 전에 아무것도 안 불러온다, "load" 는 `<img>`. */
15
+ images?: "link" | "load";
16
+ /** 링크·이미지 주소로 받는 스킴(콜론 없이). 주면 그것만 받는다. 기본 ["http","https","mailto"]. */
17
+ schemes?: string[];
18
+ };
8
19
  }
9
20
 
10
21
 
@@ -27,7 +38,7 @@ export class Rendered {
27
38
  }
28
39
 
29
40
  /**
30
- * 정규화가 고친 것의 개수.
41
+ * 정규화가 고친 것과 채널에 맞춰 바꾼 것의 개수.
31
42
  */
32
43
  export class Repairs {
33
44
  private constructor();
@@ -41,14 +52,38 @@ export class Repairs {
41
52
  * 문서 끝까지 안 닫혀서 닫아 준 코드펜스.
42
53
  */
43
54
  closedFence: number;
55
+ /**
56
+ * 다른 표기로 바꿔 쓴 강조 마커와 `<url|텍스트>` 링크.
57
+ */
58
+ convertedMarker: number;
44
59
  /**
45
60
  * 짝 잃은 채 버린 `**`.
46
61
  */
47
62
  droppedMarker: number;
63
+ /**
64
+ * 채널이 구문으로 읽을 글자를 탈출한 수(GitHub 의 `\~`·`\<`·`\*`).
65
+ */
66
+ escapedChar: number;
48
67
  /**
49
68
  * 짝이 없어 글자로 되돌린 백틱 런.
50
69
  */
51
70
  revertedCodeSpan: number;
71
+ /**
72
+ * 다른 기호로 바꿔 쓴 목록 불릿.
73
+ */
74
+ rewrittenBullet: number;
75
+ /**
76
+ * 원문과 다른 모양으로 다시 쓴 표.
77
+ */
78
+ rewrittenTable: number;
79
+ /**
80
+ * 벗긴 원문 HTML(태그·주석·줄바꿈으로 바꾼 `<br>`).
81
+ */
82
+ strippedHtml: number;
83
+ /**
84
+ * 마커 대신 태그로 낸 강조(GitHub 의 `<strong>`).
85
+ */
86
+ tagEmphasis: number;
52
87
  }
53
88
 
54
89
  /**
@@ -77,11 +112,28 @@ export class Streamer {
77
112
  */
78
113
  finish(): string;
79
114
  constructor(channel: string, options?: RenderOptions | null);
115
+ /**
116
+ * **지금 입력이 끝났다면 확정분 뒤에 붙을 꼬리.** `closeOpen` 과 같은 자리에 들어가지만
117
+ * 붙들고 있던 것(열린 강조, 표 행, 코드 스팬)까지 그린다. 누적본을 통째로 다시 그리는 쪽의
118
+ * 기본값이다. 추측이라 뒤 조각이 모양을 바꿀 수 있다 — 끝난 뒤 `revised()` 로 본다.
119
+ *
120
+ * ```js
121
+ * acc += s.push(chunk);
122
+ * await edit(acc + s.preview()); // 화면을 그릴 때만 부른다(열린 블록만큼 든다)
123
+ * acc += s.finish();
124
+ * if (s.revised()) await edit(acc); // 마지막 화면이 곧 완성본이면 건너뛴다
125
+ * ```
126
+ */
127
+ preview(): string;
80
128
  push(chunk: string): string;
81
129
  /**
82
130
  * 지금까지 정규화가 고친 것. `finish` 뒤에 보면 문서 전체의 값이다.
83
131
  */
84
132
  repairs(): Repairs;
133
+ /**
134
+ * 완성본이 마지막 `preview()` 와 다른가 — `finish` 뒤에 본다.
135
+ */
136
+ revised(): boolean;
85
137
  }
86
138
 
87
139
  /**
@@ -47,7 +47,7 @@ export class Rendered {
47
47
  if (Symbol.dispose) Rendered.prototype[Symbol.dispose] = Rendered.prototype.free;
48
48
 
49
49
  /**
50
- * 정규화가 고친 것의 개수.
50
+ * 정규화가 고친 것과 채널에 맞춰 바꾼 것의 개수.
51
51
  */
52
52
  export class Repairs {
53
53
  static __wrap(ptr) {
@@ -82,6 +82,14 @@ export class Repairs {
82
82
  const ret = wasm.__wbg_get_repairs_closedFence(this.__wbg_ptr);
83
83
  return ret >>> 0;
84
84
  }
85
+ /**
86
+ * 다른 표기로 바꿔 쓴 강조 마커와 `<url|텍스트>` 링크.
87
+ * @returns {number}
88
+ */
89
+ get convertedMarker() {
90
+ const ret = wasm.__wbg_get_repairs_convertedMarker(this.__wbg_ptr);
91
+ return ret >>> 0;
92
+ }
85
93
  /**
86
94
  * 짝 잃은 채 버린 `**`.
87
95
  * @returns {number}
@@ -90,6 +98,14 @@ export class Repairs {
90
98
  const ret = wasm.__wbg_get_repairs_droppedMarker(this.__wbg_ptr);
91
99
  return ret >>> 0;
92
100
  }
101
+ /**
102
+ * 채널이 구문으로 읽을 글자를 탈출한 수(GitHub 의 `\~`·`\<`·`\*`).
103
+ * @returns {number}
104
+ */
105
+ get escapedChar() {
106
+ const ret = wasm.__wbg_get_repairs_escapedChar(this.__wbg_ptr);
107
+ return ret >>> 0;
108
+ }
93
109
  /**
94
110
  * 짝이 없어 글자로 되돌린 백틱 런.
95
111
  * @returns {number}
@@ -98,6 +114,38 @@ export class Repairs {
98
114
  const ret = wasm.__wbg_get_repairs_revertedCodeSpan(this.__wbg_ptr);
99
115
  return ret >>> 0;
100
116
  }
117
+ /**
118
+ * 다른 기호로 바꿔 쓴 목록 불릿.
119
+ * @returns {number}
120
+ */
121
+ get rewrittenBullet() {
122
+ const ret = wasm.__wbg_get_repairs_rewrittenBullet(this.__wbg_ptr);
123
+ return ret >>> 0;
124
+ }
125
+ /**
126
+ * 원문과 다른 모양으로 다시 쓴 표.
127
+ * @returns {number}
128
+ */
129
+ get rewrittenTable() {
130
+ const ret = wasm.__wbg_get_repairs_rewrittenTable(this.__wbg_ptr);
131
+ return ret >>> 0;
132
+ }
133
+ /**
134
+ * 벗긴 원문 HTML(태그·주석·줄바꿈으로 바꾼 `<br>`).
135
+ * @returns {number}
136
+ */
137
+ get strippedHtml() {
138
+ const ret = wasm.__wbg_get_repairs_strippedHtml(this.__wbg_ptr);
139
+ return ret >>> 0;
140
+ }
141
+ /**
142
+ * 마커 대신 태그로 낸 강조(GitHub 의 `<strong>`).
143
+ * @returns {number}
144
+ */
145
+ get tagEmphasis() {
146
+ const ret = wasm.__wbg_get_repairs_tagEmphasis(this.__wbg_ptr);
147
+ return ret >>> 0;
148
+ }
101
149
  /**
102
150
  * 블록이 끝나도록 안 닫혀서 닫아 준 강조.
103
151
  * @param {number} arg0
@@ -112,6 +160,13 @@ export class Repairs {
112
160
  set closedFence(arg0) {
113
161
  wasm.__wbg_set_repairs_closedFence(this.__wbg_ptr, arg0);
114
162
  }
163
+ /**
164
+ * 다른 표기로 바꿔 쓴 강조 마커와 `<url|텍스트>` 링크.
165
+ * @param {number} arg0
166
+ */
167
+ set convertedMarker(arg0) {
168
+ wasm.__wbg_set_repairs_convertedMarker(this.__wbg_ptr, arg0);
169
+ }
115
170
  /**
116
171
  * 짝 잃은 채 버린 `**`.
117
172
  * @param {number} arg0
@@ -119,6 +174,13 @@ export class Repairs {
119
174
  set droppedMarker(arg0) {
120
175
  wasm.__wbg_set_repairs_droppedMarker(this.__wbg_ptr, arg0);
121
176
  }
177
+ /**
178
+ * 채널이 구문으로 읽을 글자를 탈출한 수(GitHub 의 `\~`·`\<`·`\*`).
179
+ * @param {number} arg0
180
+ */
181
+ set escapedChar(arg0) {
182
+ wasm.__wbg_set_repairs_escapedChar(this.__wbg_ptr, arg0);
183
+ }
122
184
  /**
123
185
  * 짝이 없어 글자로 되돌린 백틱 런.
124
186
  * @param {number} arg0
@@ -126,6 +188,34 @@ export class Repairs {
126
188
  set revertedCodeSpan(arg0) {
127
189
  wasm.__wbg_set_repairs_revertedCodeSpan(this.__wbg_ptr, arg0);
128
190
  }
191
+ /**
192
+ * 다른 기호로 바꿔 쓴 목록 불릿.
193
+ * @param {number} arg0
194
+ */
195
+ set rewrittenBullet(arg0) {
196
+ wasm.__wbg_set_repairs_rewrittenBullet(this.__wbg_ptr, arg0);
197
+ }
198
+ /**
199
+ * 원문과 다른 모양으로 다시 쓴 표.
200
+ * @param {number} arg0
201
+ */
202
+ set rewrittenTable(arg0) {
203
+ wasm.__wbg_set_repairs_rewrittenTable(this.__wbg_ptr, arg0);
204
+ }
205
+ /**
206
+ * 벗긴 원문 HTML(태그·주석·줄바꿈으로 바꾼 `<br>`).
207
+ * @param {number} arg0
208
+ */
209
+ set strippedHtml(arg0) {
210
+ wasm.__wbg_set_repairs_strippedHtml(this.__wbg_ptr, arg0);
211
+ }
212
+ /**
213
+ * 마커 대신 태그로 낸 강조(GitHub 의 `<strong>`).
214
+ * @param {number} arg0
215
+ */
216
+ set tagEmphasis(arg0) {
217
+ wasm.__wbg_set_repairs_tagEmphasis(this.__wbg_ptr, arg0);
218
+ }
129
219
  }
130
220
  if (Symbol.dispose) Repairs.prototype[Symbol.dispose] = Repairs.prototype.free;
131
221
 
@@ -217,6 +307,35 @@ export class Streamer {
217
307
  wasm.__wbindgen_add_to_stack_pointer(16);
218
308
  }
219
309
  }
310
+ /**
311
+ * **지금 입력이 끝났다면 확정분 뒤에 붙을 꼬리.** `closeOpen` 과 같은 자리에 들어가지만
312
+ * 붙들고 있던 것(열린 강조, 표 행, 코드 스팬)까지 그린다. 누적본을 통째로 다시 그리는 쪽의
313
+ * 기본값이다. 추측이라 뒤 조각이 모양을 바꿀 수 있다 — 끝난 뒤 `revised()` 로 본다.
314
+ *
315
+ * ```js
316
+ * acc += s.push(chunk);
317
+ * await edit(acc + s.preview()); // 화면을 그릴 때만 부른다(열린 블록만큼 든다)
318
+ * acc += s.finish();
319
+ * if (s.revised()) await edit(acc); // 마지막 화면이 곧 완성본이면 건너뛴다
320
+ * ```
321
+ * @returns {string}
322
+ */
323
+ preview() {
324
+ let deferred1_0;
325
+ let deferred1_1;
326
+ try {
327
+ const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
328
+ wasm.streamer_preview(retptr, this.__wbg_ptr);
329
+ var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true);
330
+ var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true);
331
+ deferred1_0 = r0;
332
+ deferred1_1 = r1;
333
+ return getStringFromWasm0(r0, r1);
334
+ } finally {
335
+ wasm.__wbindgen_add_to_stack_pointer(16);
336
+ wasm.__wbindgen_export3(deferred1_0, deferred1_1, 1);
337
+ }
338
+ }
220
339
  /**
221
340
  * @param {string} chunk
222
341
  * @returns {string}
@@ -247,6 +366,14 @@ export class Streamer {
247
366
  const ret = wasm.streamer_repairs(this.__wbg_ptr);
248
367
  return Repairs.__wrap(ret);
249
368
  }
369
+ /**
370
+ * 완성본이 마지막 `preview()` 와 다른가 — `finish` 뒤에 본다.
371
+ * @returns {boolean}
372
+ */
373
+ revised() {
374
+ const ret = wasm.streamer_revised(this.__wbg_ptr);
375
+ return ret !== 0;
376
+ }
250
377
  }
251
378
  if (Symbol.dispose) Streamer.prototype[Symbol.dispose] = Streamer.prototype.free;
252
379
 
@@ -334,16 +461,54 @@ export function __wbg_Error_67e7344beaa85059(arg0, arg1) {
334
461
  const ret = Error(getStringFromWasm0(arg0, arg1));
335
462
  return addHeapObject(ret);
336
463
  }
464
+ export function __wbg___wbindgen_string_get_92ab86bb19cbc12f(arg0, arg1) {
465
+ const obj = getObject(arg1);
466
+ const ret = typeof(obj) === 'string' ? obj : undefined;
467
+ var ptr1 = isLikeNone(ret) ? 0 : passStringToWasm0(ret, wasm.__wbindgen_export, wasm.__wbindgen_export2);
468
+ var len1 = WASM_VECTOR_LEN;
469
+ getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true);
470
+ getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true);
471
+ }
337
472
  export function __wbg___wbindgen_throw_5d9e815e6fdf150f(arg0, arg1) {
338
473
  throw new Error(getStringFromWasm0(arg0, arg1));
339
474
  }
340
- export function __wbg_from_5ceccc568e2b01b6(arg0, arg1) {
475
+ export function __wbg_from_6a18a824ab73bfc5(arg0, arg1) {
341
476
  const ret = getObject(arg1).from;
342
477
  var ptr1 = isLikeNone(ret) ? 0 : passStringToWasm0(ret, wasm.__wbindgen_export, wasm.__wbindgen_export2);
343
478
  var len1 = WASM_VECTOR_LEN;
344
479
  getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true);
345
480
  getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true);
346
481
  }
482
+ export function __wbg_html_a628fe022a5daeaf(arg0) {
483
+ const ret = getObject(arg0).html;
484
+ return isLikeNone(ret) ? 0 : addHeapObject(ret);
485
+ }
486
+ export function __wbg_images_ff5cc833afad2dcf(arg0, arg1) {
487
+ const ret = getObject(arg1).images;
488
+ var ptr1 = isLikeNone(ret) ? 0 : passStringToWasm0(ret, wasm.__wbindgen_export, wasm.__wbindgen_export2);
489
+ var len1 = WASM_VECTOR_LEN;
490
+ getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true);
491
+ getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true);
492
+ }
493
+ export function __wbg_limit_cce570f9381001ea(arg0, arg1) {
494
+ const ret = getObject(arg1).limit;
495
+ getDataViewMemory0().setFloat64(arg0 + 8 * 1, isLikeNone(ret) ? 0 : ret, true);
496
+ getDataViewMemory0().setInt32(arg0 + 4 * 0, !isLikeNone(ret), true);
497
+ }
498
+ export function __wbg_lineBreaks_aa928a165d0a7659(arg0, arg1) {
499
+ const ret = getObject(arg1).lineBreaks;
500
+ var ptr1 = isLikeNone(ret) ? 0 : passStringToWasm0(ret, wasm.__wbindgen_export, wasm.__wbindgen_export2);
501
+ var len1 = WASM_VECTOR_LEN;
502
+ getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true);
503
+ getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true);
504
+ }
505
+ export function __wbg_schemes_429ccca2fff29ed9(arg0, arg1) {
506
+ const ret = getObject(arg1).schemes;
507
+ var ptr1 = isLikeNone(ret) ? 0 : passArrayJsValueToWasm0(ret, wasm.__wbindgen_export);
508
+ var len1 = WASM_VECTOR_LEN;
509
+ getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true);
510
+ getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true);
511
+ }
347
512
  export function __wbindgen_generic_0000000000000001(arg0, arg1) {
348
513
  // Cast intrinsic for `Ref(String) -> Externref`.
349
514
  const ret = getStringFromWasm0(arg0, arg1);
@@ -418,6 +583,16 @@ function isLikeNone(x) {
418
583
  return x === undefined || x === null;
419
584
  }
420
585
 
586
+ function passArrayJsValueToWasm0(array, malloc) {
587
+ const ptr = malloc(array.length * 4, 4) >>> 0;
588
+ const mem = getDataViewMemory0();
589
+ for (let i = 0; i < array.length; i++) {
590
+ mem.setUint32(ptr + 4 * i, addHeapObject(array[i]), true);
591
+ }
592
+ WASM_VECTOR_LEN = array.length;
593
+ return ptr;
594
+ }
595
+
421
596
  function passStringToWasm0(arg, malloc, realloc) {
422
597
  if (realloc === undefined) {
423
598
  const buf = cachedTextEncoder.encode(arg);
Binary file
@@ -3,14 +3,26 @@
3
3
  export const memory: WebAssembly.Memory;
4
4
  export const __wbg_get_repairs_closedEmphasis: (a: number) => number;
5
5
  export const __wbg_get_repairs_closedFence: (a: number) => number;
6
+ export const __wbg_get_repairs_convertedMarker: (a: number) => number;
6
7
  export const __wbg_get_repairs_droppedMarker: (a: number) => number;
8
+ export const __wbg_get_repairs_escapedChar: (a: number) => number;
7
9
  export const __wbg_get_repairs_revertedCodeSpan: (a: number) => number;
10
+ export const __wbg_get_repairs_rewrittenBullet: (a: number) => number;
11
+ export const __wbg_get_repairs_rewrittenTable: (a: number) => number;
12
+ export const __wbg_get_repairs_strippedHtml: (a: number) => number;
13
+ export const __wbg_get_repairs_tagEmphasis: (a: number) => number;
8
14
  export const __wbg_rendered_free: (a: number, b: number) => void;
9
15
  export const __wbg_repairs_free: (a: number, b: number) => void;
10
16
  export const __wbg_set_repairs_closedEmphasis: (a: number, b: number) => void;
11
17
  export const __wbg_set_repairs_closedFence: (a: number, b: number) => void;
18
+ export const __wbg_set_repairs_convertedMarker: (a: number, b: number) => void;
12
19
  export const __wbg_set_repairs_droppedMarker: (a: number, b: number) => void;
20
+ export const __wbg_set_repairs_escapedChar: (a: number, b: number) => void;
13
21
  export const __wbg_set_repairs_revertedCodeSpan: (a: number, b: number) => void;
22
+ export const __wbg_set_repairs_rewrittenBullet: (a: number, b: number) => void;
23
+ export const __wbg_set_repairs_rewrittenTable: (a: number, b: number) => void;
24
+ export const __wbg_set_repairs_strippedHtml: (a: number, b: number) => void;
25
+ export const __wbg_set_repairs_tagEmphasis: (a: number, b: number) => void;
14
26
  export const __wbg_streamer_free: (a: number, b: number) => void;
15
27
  export const limit: (a: number, b: number, c: number) => void;
16
28
  export const render: (a: number, b: number, c: number, d: number, e: number, f: number) => void;
@@ -20,8 +32,10 @@ export const rendered_repairs: (a: number) => number;
20
32
  export const streamer_closeOpen: (a: number, b: number) => void;
21
33
  export const streamer_finish: (a: number, b: number) => void;
22
34
  export const streamer_new: (a: number, b: number, c: number, d: number) => void;
35
+ export const streamer_preview: (a: number, b: number) => void;
23
36
  export const streamer_push: (a: number, b: number, c: number, d: number) => void;
24
37
  export const streamer_repairs: (a: number) => number;
38
+ export const streamer_revised: (a: number) => number;
25
39
  export const __wbindgen_export: (a: number, b: number) => number;
26
40
  export const __wbindgen_export2: (a: number, b: number, c: number, d: number) => number;
27
41
  export const __wbindgen_add_to_stack_pointer: (a: number) => number;
package/js/events.d.ts ADDED
@@ -0,0 +1,20 @@
1
+ /** mdwire html 출력에 나오는 태그. */
2
+ export type MdTag =
3
+ | "p" | "h1" | "h2" | "h3" | "h4" | "h5" | "h6" | "ul" | "ol" | "li" | "blockquote" | "pre" | "code"
4
+ | "strong" | "em" | "del" | "a" | "table" | "thead" | "tbody" | "tr" | "th" | "td" | "hr" | "br"
5
+ | "sub" | "sup" | "b" | "i" | "u" | "s" | "strike" | "span" | "small" | "mark" | "kbd" | "ins" | "img";
6
+
7
+ /**
8
+ * 이벤트 하나. 속성은 코어가 내는 것만 담긴다 —
9
+ * `a` 의 `href`, `code` 의 `class`(`language-…`), `th`·`td` 의 `style`(`text-align:…`), `ol` 의 `start`.
10
+ */
11
+ export type MdEvent =
12
+ | { type: "open"; tag: MdTag; attrs: Record<string, string> }
13
+ | { type: "close"; tag: MdTag }
14
+ | { type: "void"; tag: "br" | "hr" }
15
+ /** 이미지는 `html.images: "load"` 일 때만 나온다. `src`·`alt` 를 담는다. */
16
+ | { type: "void"; tag: "img"; attrs: Record<string, string> }
17
+ | { type: "text"; text: string };
18
+
19
+ /** html 채널 출력(또는 스트리밍 누적본 + `closeOpen()`)을 이벤트 배열로 푼다. */
20
+ export function toEvents(html: string): MdEvent[];
package/js/events.js ADDED
@@ -0,0 +1,70 @@
1
+ // mdwire 의 html 채널 출력을 이벤트 열로 푼다.
2
+ //
3
+ // **mdwire 가 낸 HTML 만 읽는다.** 코어가 글자를 전부 escape 하고 태그를 고정된 집합으로만
4
+ // 내므로(SPEC 4절) 여기서 필요한 것은 작은 토크나이저 하나다. 남이 쓴 HTML 을 넣으면 모르는
5
+ // 태그는 글자로 돌려준다 — 이 모듈은 새니타이저가 아니다. 안전 판단은 코어 한 곳에 있다.
6
+
7
+ /** 출력에 나올 수 있는 태그. 이 밖의 것은 글자다. */
8
+ const TAGS = new Set([
9
+ "p", "h1", "h2", "h3", "h4", "h5", "h6", "ul", "ol", "li", "blockquote", "pre", "code",
10
+ "strong", "em", "del", "a", "table", "thead", "tbody", "tr", "th", "td", "hr", "br",
11
+ "sub", "sup", "b", "i", "u", "s", "strike", "span", "small", "mark", "kbd", "ins", "img",
12
+ ]);
13
+ const VOID = new Set(["br", "hr", "img"]);
14
+ /** 태그마다 읽는 속성. 코어가 내는 것만 — 나머지는 버린다. */
15
+ const ATTRS = { a: ["href"], code: ["class"], th: ["style"], td: ["style"], ol: ["start"], img: ["src", "alt"] };
16
+
17
+ const ENTITIES = { amp: "&", lt: "<", gt: ">", quot: '"' };
18
+ const decode = (s) => s.replace(/&(amp|lt|gt|quot);/g, (_, n) => ENTITIES[n]);
19
+
20
+ /**
21
+ * html 채널 출력을 이벤트 배열로 바꾼다.
22
+ * @param {string} html `render(text, "html")` 의 결과, 또는 스트리밍 누적본 + `closeOpen()`.
23
+ * @returns {import("./events.d.ts").MdEvent[]}
24
+ */
25
+ export function toEvents(html) {
26
+ const events = [];
27
+ let text = "";
28
+ const flush = () => {
29
+ if (text) {
30
+ events.push({ type: "text", text: decode(text) });
31
+ text = "";
32
+ }
33
+ };
34
+ let i = 0;
35
+ while (i < html.length) {
36
+ const lt = html.indexOf("<", i);
37
+ if (lt < 0) {
38
+ text += html.slice(i);
39
+ break;
40
+ }
41
+ text += html.slice(i, lt);
42
+ const gt = html.indexOf(">", lt);
43
+ const m = gt < 0 ? null : /^<(\/?)([a-z][a-z0-9]*)((?:\s+[a-z-]+="[^"]*")*)\s*\/?>$/.exec(html.slice(lt, gt + 1));
44
+ if (!m || !TAGS.has(m[2])) {
45
+ text += "<";
46
+ i = lt + 1;
47
+ continue;
48
+ }
49
+ flush();
50
+ const [, closing, tag, rawAttrs] = m;
51
+ if (closing) {
52
+ events.push({ type: "close", tag });
53
+ } else if (VOID.has(tag)) {
54
+ const attrs = {};
55
+ for (const [, name, value] of rawAttrs.matchAll(/([a-z-]+)="([^"]*)"/g)) {
56
+ if (ATTRS[tag]?.includes(name)) attrs[name] = decode(value);
57
+ }
58
+ events.push(tag === "img" ? { type: "void", tag, attrs } : { type: "void", tag });
59
+ } else {
60
+ const attrs = {};
61
+ for (const [, name, value] of rawAttrs.matchAll(/([a-z-]+)="([^"]*)"/g)) {
62
+ if (ATTRS[tag]?.includes(name)) attrs[name] = decode(value);
63
+ }
64
+ events.push({ type: "open", tag, attrs });
65
+ }
66
+ i = gt + 1;
67
+ }
68
+ flush();
69
+ return events;
70
+ }
package/js/react.d.ts ADDED
@@ -0,0 +1,43 @@
1
+ import type { ElementType, ReactElement, ReactNode } from "react";
2
+ import type { MdTag } from "./events.js";
3
+ import type { RenderOptions } from "@minjun0219/mdwire";
4
+
5
+ /** 태그별로 갈아 끼울 컴포넌트 — 예: `{ a: MyLink, code: CodeBlock }`. 받는 props 는 그 태그의 것이다. */
6
+ export type MdComponents = Partial<Record<MdTag, ElementType>>;
7
+
8
+ export interface MarkdownProps {
9
+ /** 에이전트가 쓴 마크다운(완성된 글). 스트리밍은 `Streamer` + `toElements` 로 — 누적본을 매번 넘기면 처음부터 다시 변환한다. */
10
+ text: string;
11
+ /** 입력 표기. 슬랙 레거시 mrkdwn 이면 "slack-mrkdwn". */
12
+ from?: "markdown" | "slack-mrkdwn";
13
+ components?: MdComponents;
14
+ /** 코어에 넘길 옵션 — `html` 정책(줄바꿈·이미지·스킴)이 여기 든다. `from` 은 위 것이 이긴다. */
15
+ options?: RenderOptions;
16
+ }
17
+
18
+ /** 마크다운을 React 요소로 그린다. innerHTML 을 쓰지 않는다. */
19
+ export function Markdown(props: MarkdownProps): ReactElement;
20
+
21
+ export interface MarkdownStreamOptions {
22
+ from?: "markdown" | "slack-mrkdwn";
23
+ components?: MdComponents;
24
+ options?: RenderOptions;
25
+ /** 붙든 것(열린 강조·표 행·코드 스팬)도 먼저 그린다. 기본 `true`. `false` 면 확정된 것만. */
26
+ eager?: boolean;
27
+ /** 끝났을 때. `revised` 는 완성본이 마지막 화면과 다른가 — 거짓이면 훅은 다시 그리지 않는다. */
28
+ onSettled?: (html: string, revised: boolean) => void;
29
+ }
30
+
31
+ /**
32
+ * 스트리밍용 훅 — 토큰마다 `push`, 끝나면 `finish`. 기본은 붙든 것도 먼저 그린다(`eager`).
33
+ * `onSettled` 말고는 처음 한 번만 읽는다.
34
+ */
35
+ export function useMarkdownStream(opts?: MarkdownStreamOptions): {
36
+ elements: ReactElement;
37
+ push(chunk: string): void;
38
+ finish(): void;
39
+ };
40
+
41
+ /** html 채널 출력(스트리밍이면 누적본 + `preview()` 또는 `closeOpen()`)을 React 노드로 바꾼다. */
42
+ /** `schemes` 는 코어에 준 `options.html.schemes` 와 같게 준다. 기본 `["http", "https", "mailto"]`. */
43
+ export function toElements(html: string, components?: MdComponents, schemes?: string[]): ReactNode[];
package/js/react.js ADDED
@@ -0,0 +1,154 @@
1
+ // mdwire → React 요소. innerHTML 없이 createElement 로 조립한다.
2
+ //
3
+ // 안전 판단은 코어의 html 채널이 한다(글자 escape, 태그 고정, 링크 스킴). 여기서는 그 출력을
4
+ // 이벤트로 풀어 요소로 세울 뿐이다 — 글자는 React 가 글자로 넣으니 다시 escape 할 것도 없다.
5
+ // 링크·이미지 스킴만 한 번 더 본다: 소비자가 `a` 를 자기 컴포넌트로 갈아 끼워도 주소가 안전하도록.
6
+ // 목록은 코어에 준 것(`options.html.schemes`)과 같다 — 다르면 코어가 허용한 링크를 여기서 막는다.
7
+ import { createElement, Fragment, useCallback, useEffect, useReducer, useRef } from "react";
8
+ import { render, Streamer } from "@minjun0219/mdwire";
9
+ import { toEvents } from "./events.js";
10
+
11
+ const DEFAULT_SCHEMES = ["http", "https", "mailto"];
12
+
13
+ function allowed(url, schemes) {
14
+ const u = url.trimStart();
15
+ const colon = u.indexOf(":");
16
+ if (colon < 1) return false;
17
+ const scheme = u.slice(0, colon).toLowerCase();
18
+ return schemes.some((s) => s.toLowerCase() === scheme);
19
+ }
20
+ /** 이 안에서는 공백만 있는 글이 DOM 규칙상 자식이 될 수 없다(React 가 경고한다). */
21
+ const NO_TEXT = new Set(["ul", "ol", "table", "thead", "tbody", "tr"]);
22
+
23
+ function propsOf(tag, attrs, key, schemes) {
24
+ const props = { key };
25
+ if (tag === "a" && attrs.href && allowed(attrs.href, schemes)) props.href = attrs.href;
26
+ if (tag === "img" && attrs.src && allowed(attrs.src, schemes)) {
27
+ props.src = attrs.src;
28
+ props.alt = attrs.alt ?? "";
29
+ }
30
+ if (tag === "code" && attrs.class) props.className = attrs.class;
31
+ if ((tag === "th" || tag === "td") && attrs.style) {
32
+ const align = /^text-align:(left|right|center)$/.exec(attrs.style);
33
+ if (align) props.style = { textAlign: align[1] };
34
+ }
35
+ if (tag === "ol" && attrs.start) {
36
+ const n = Number(attrs.start);
37
+ if (Number.isInteger(n) && n > 0) props.start = n;
38
+ }
39
+ return props;
40
+ }
41
+
42
+ /**
43
+ * html 채널 출력을 React 노드 배열로 바꾼다. 스트리밍 누적본을 직접 다루는 쪽이 쓴다 —
44
+ * `toElements(acc + streamer.closeOpen())`.
45
+ * @param {string} html
46
+ * @param {Partial<Record<string, import("react").ElementType>>} [components] 태그별로 갈아 끼울 컴포넌트.
47
+ * @param {string[]} [schemes] 링크·이미지로 받는 스킴. 코어에 준 `options.html.schemes` 와 같게.
48
+ */
49
+ export function toElements(html, components = {}, schemes = DEFAULT_SCHEMES) {
50
+ const root = { tag: null, children: [] };
51
+ const stack = [root];
52
+ let key = 0;
53
+ const make = (tag, attrs, children) =>
54
+ createElement(components[tag] ?? tag, propsOf(tag, attrs, key++, schemes), ...children);
55
+ for (const ev of toEvents(html)) {
56
+ const top = stack[stack.length - 1];
57
+ switch (ev.type) {
58
+ case "text":
59
+ if (!(NO_TEXT.has(top.tag) && ev.text.trim() === "")) top.children.push(ev.text);
60
+ break;
61
+ case "void":
62
+ // 주소가 안전하지 않은 이미지는 아예 세우지 않는다 — 빈 `<img>` 도 쓸모가 없다.
63
+ if (ev.tag === "img" && !allowed(ev.attrs?.src ?? "", schemes)) break;
64
+ top.children.push(make(ev.tag, ev.attrs ?? {}, []));
65
+ break;
66
+ case "open":
67
+ stack.push({ tag: ev.tag, attrs: ev.attrs, children: [] });
68
+ break;
69
+ case "close": {
70
+ // 코어 출력은 짝이 맞는다. 안 맞는 닫기는 버린다 — 남이 넣은 글이라도 트리는 선다.
71
+ if (top.tag !== ev.tag || stack.length === 1) break;
72
+ stack.pop();
73
+ stack[stack.length - 1].children.push(make(top.tag, top.attrs, top.children));
74
+ break;
75
+ }
76
+ }
77
+ }
78
+ // 안 닫힌 것은 닫는다(스트리밍 중 closeOpen 을 안 붙인 누적본).
79
+ while (stack.length > 1) {
80
+ const top = stack.pop();
81
+ stack[stack.length - 1].children.push(make(top.tag, top.attrs, top.children));
82
+ }
83
+ return root.children;
84
+ }
85
+
86
+ /**
87
+ * 에이전트 마크다운을 그린다 — 완성된 글용. 스트리밍 중인 누적본을 토큰마다 넘기면 매번
88
+ * 처음부터 다시 변환해 전체 비용이 제곱으로 늘고, 반쪽 마커(`**굵`, 여는 백틱, `##`)가 잠깐 글자로
89
+ * 보였다가 사라진다. 스트리밍은 [`useMarkdownStream`].
90
+ * @param {import("./react.d.ts").MarkdownProps} props
91
+ */
92
+ export function Markdown({ text, from, components, options }) {
93
+ const opts = { ...options, ...(from ? { from } : {}) };
94
+ const html = render(text, "html", opts).join("");
95
+ return createElement(Fragment, null, ...toElements(html, components, opts.html?.schemes ?? DEFAULT_SCHEMES));
96
+ }
97
+
98
+ /**
99
+ * 스트리밍용 훅. 토큰이 오는 대로 `push(chunk)`, 끝나면 `finish()`.
100
+ *
101
+ * 기본(`eager: true`)은 **붙든 것도 먼저 그린다** — 열린 강조는 닫아서, 표는 지금까지 온 행으로,
102
+ * 코드 스팬은 닫아서(`Streamer.preview()`). 추측이라 뒤 토큰이 모양을 바꿀 수 있지만 완성본은
103
+ * 일괄 변환과 같다. `eager: false` 면 확정된 것만 보인다(append-only, SPEC 8.2) — 짝이 안 맞은
104
+ * 강조·판정 전 접두사는 확정될 때 나온다.
105
+ *
106
+ * 끝나면 `onSettled(html, revised)` — `revised` 는 완성본이 마지막으로 **화면에 그려진** 것과
107
+ * 다른가다. 같으면 훅은 다시 그리지 않는다.
108
+ * @param {import("./react.d.ts").MarkdownStreamOptions} [opts]
109
+ */
110
+ export function useMarkdownStream({ from, components, options, eager = true, onSettled } = {}) {
111
+ const ref = useRef(null);
112
+ const init = useRef(null);
113
+ const [, rerender] = useReducer((n) => n + 1, 0);
114
+ if (init.current === null) {
115
+ const o = { ...options, ...(from ? { from } : {}) };
116
+ init.current = () => ({ streamer: new Streamer("html", o), acc: "", done: false, eager, schemes: o.html?.schemes ?? DEFAULT_SCHEMES });
117
+ }
118
+ // 해제됐으면 다시 만든다 — StrictMode 개발 모드는 이펙트를 붙였다 떼었다 다시 붙여서, 정리
119
+ // 함수가 스트리머를 먼저 해제한다(토큰이 오기 전이라 잃는 것은 없다).
120
+ const state = () => (ref.current ??= init.current());
121
+ state().onSettled = onSettled; // 콜백은 매번 최신 것을 본다
122
+ // wasm 메모리를 돌려준다 — 스트리머는 JS 가비지 컬렉터가 모르는 곳에 산다.
123
+ useEffect(
124
+ () => () => {
125
+ ref.current?.streamer.free();
126
+ ref.current = null;
127
+ },
128
+ [],
129
+ );
130
+ const push = useCallback((chunk) => {
131
+ const st = state();
132
+ if (st.done) return;
133
+ st.acc += st.streamer.push(chunk);
134
+ rerender();
135
+ }, []);
136
+ const finish = useCallback(() => {
137
+ const st = state();
138
+ if (st.done) return;
139
+ st.acc += st.streamer.finish();
140
+ st.done = true;
141
+ // 스트리머의 revised() 가 아니라 커밋된 화면과 비교한다. 렌더는 버려질 수 있어서(concurrent
142
+ // 렌더) 마지막으로 preview() 를 부른 렌더가 화면에 올라갔다는 보장이 없다.
143
+ const revised = st.acc !== st.shown;
144
+ st.onSettled?.(st.acc, revised);
145
+ if (revised) rerender();
146
+ }, []);
147
+ const st = state();
148
+ const tail = st.done ? "" : st.eager ? st.streamer.preview() : st.streamer.closeOpen();
149
+ const html = st.acc + tail;
150
+ useEffect(() => {
151
+ if (ref.current) ref.current.shown = html;
152
+ });
153
+ return { elements: createElement(Fragment, null, ...toElements(html, components, st.schemes)), push, finish };
154
+ }
package/node/mdwire.d.ts CHANGED
@@ -5,6 +5,17 @@
5
5
  export interface RenderOptions {
6
6
  /** 입력 표기. 슬랙 레거시 mrkdwn(`*굵게*` `~취소~`)으로 쓴 에이전트 출력이면 "slack-mrkdwn". */
7
7
  from?: "markdown" | "slack-mrkdwn";
8
+ /** 조각 한도(글자 수). 생략하면 채널의 한도다 — plain 을 텔레그램에 보내면 4096. 스트리밍은 나누지 않는다. */
9
+ limit?: number;
10
+ /** 브라우저 채널("html")의 정책. 기본값이 가장 보수적이다. */
11
+ html?: {
12
+ /** 블록 안 줄바꿈. "br"(기본) 은 `<br>`, "space" 는 브라우저가 공백으로 접게 둔다. */
13
+ lineBreaks?: "br" | "space";
14
+ /** 이미지. "link"(기본) 는 누르기 전에 아무것도 안 불러온다, "load" 는 `<img>`. */
15
+ images?: "link" | "load";
16
+ /** 링크·이미지 주소로 받는 스킴(콜론 없이). 주면 그것만 받는다. 기본 ["http","https","mailto"]. */
17
+ schemes?: string[];
18
+ };
8
19
  }
9
20
 
10
21
 
@@ -27,7 +38,7 @@ export class Rendered {
27
38
  }
28
39
 
29
40
  /**
30
- * 정규화가 고친 것의 개수.
41
+ * 정규화가 고친 것과 채널에 맞춰 바꾼 것의 개수.
31
42
  */
32
43
  export class Repairs {
33
44
  private constructor();
@@ -41,14 +52,38 @@ export class Repairs {
41
52
  * 문서 끝까지 안 닫혀서 닫아 준 코드펜스.
42
53
  */
43
54
  closedFence: number;
55
+ /**
56
+ * 다른 표기로 바꿔 쓴 강조 마커와 `<url|텍스트>` 링크.
57
+ */
58
+ convertedMarker: number;
44
59
  /**
45
60
  * 짝 잃은 채 버린 `**`.
46
61
  */
47
62
  droppedMarker: number;
63
+ /**
64
+ * 채널이 구문으로 읽을 글자를 탈출한 수(GitHub 의 `\~`·`\<`·`\*`).
65
+ */
66
+ escapedChar: number;
48
67
  /**
49
68
  * 짝이 없어 글자로 되돌린 백틱 런.
50
69
  */
51
70
  revertedCodeSpan: number;
71
+ /**
72
+ * 다른 기호로 바꿔 쓴 목록 불릿.
73
+ */
74
+ rewrittenBullet: number;
75
+ /**
76
+ * 원문과 다른 모양으로 다시 쓴 표.
77
+ */
78
+ rewrittenTable: number;
79
+ /**
80
+ * 벗긴 원문 HTML(태그·주석·줄바꿈으로 바꾼 `<br>`).
81
+ */
82
+ strippedHtml: number;
83
+ /**
84
+ * 마커 대신 태그로 낸 강조(GitHub 의 `<strong>`).
85
+ */
86
+ tagEmphasis: number;
52
87
  }
53
88
 
54
89
  /**
@@ -77,11 +112,28 @@ export class Streamer {
77
112
  */
78
113
  finish(): string;
79
114
  constructor(channel: string, options?: RenderOptions | null);
115
+ /**
116
+ * **지금 입력이 끝났다면 확정분 뒤에 붙을 꼬리.** `closeOpen` 과 같은 자리에 들어가지만
117
+ * 붙들고 있던 것(열린 강조, 표 행, 코드 스팬)까지 그린다. 누적본을 통째로 다시 그리는 쪽의
118
+ * 기본값이다. 추측이라 뒤 조각이 모양을 바꿀 수 있다 — 끝난 뒤 `revised()` 로 본다.
119
+ *
120
+ * ```js
121
+ * acc += s.push(chunk);
122
+ * await edit(acc + s.preview()); // 화면을 그릴 때만 부른다(열린 블록만큼 든다)
123
+ * acc += s.finish();
124
+ * if (s.revised()) await edit(acc); // 마지막 화면이 곧 완성본이면 건너뛴다
125
+ * ```
126
+ */
127
+ preview(): string;
80
128
  push(chunk: string): string;
81
129
  /**
82
130
  * 지금까지 정규화가 고친 것. `finish` 뒤에 보면 문서 전체의 값이다.
83
131
  */
84
132
  repairs(): Repairs;
133
+ /**
134
+ * 완성본이 마지막 `preview()` 와 다른가 — `finish` 뒤에 본다.
135
+ */
136
+ revised(): boolean;
85
137
  }
86
138
 
87
139
  /**
package/node/mdwire.js CHANGED
@@ -50,7 +50,7 @@ if (Symbol.dispose) Rendered.prototype[Symbol.dispose] = Rendered.prototype.free
50
50
  exports.Rendered = Rendered;
51
51
 
52
52
  /**
53
- * 정규화가 고친 것의 개수.
53
+ * 정규화가 고친 것과 채널에 맞춰 바꾼 것의 개수.
54
54
  */
55
55
  class Repairs {
56
56
  static __wrap(ptr) {
@@ -85,6 +85,14 @@ class Repairs {
85
85
  const ret = wasm.__wbg_get_repairs_closedFence(this.__wbg_ptr);
86
86
  return ret >>> 0;
87
87
  }
88
+ /**
89
+ * 다른 표기로 바꿔 쓴 강조 마커와 `<url|텍스트>` 링크.
90
+ * @returns {number}
91
+ */
92
+ get convertedMarker() {
93
+ const ret = wasm.__wbg_get_repairs_convertedMarker(this.__wbg_ptr);
94
+ return ret >>> 0;
95
+ }
88
96
  /**
89
97
  * 짝 잃은 채 버린 `**`.
90
98
  * @returns {number}
@@ -93,6 +101,14 @@ class Repairs {
93
101
  const ret = wasm.__wbg_get_repairs_droppedMarker(this.__wbg_ptr);
94
102
  return ret >>> 0;
95
103
  }
104
+ /**
105
+ * 채널이 구문으로 읽을 글자를 탈출한 수(GitHub 의 `\~`·`\<`·`\*`).
106
+ * @returns {number}
107
+ */
108
+ get escapedChar() {
109
+ const ret = wasm.__wbg_get_repairs_escapedChar(this.__wbg_ptr);
110
+ return ret >>> 0;
111
+ }
96
112
  /**
97
113
  * 짝이 없어 글자로 되돌린 백틱 런.
98
114
  * @returns {number}
@@ -101,6 +117,38 @@ class Repairs {
101
117
  const ret = wasm.__wbg_get_repairs_revertedCodeSpan(this.__wbg_ptr);
102
118
  return ret >>> 0;
103
119
  }
120
+ /**
121
+ * 다른 기호로 바꿔 쓴 목록 불릿.
122
+ * @returns {number}
123
+ */
124
+ get rewrittenBullet() {
125
+ const ret = wasm.__wbg_get_repairs_rewrittenBullet(this.__wbg_ptr);
126
+ return ret >>> 0;
127
+ }
128
+ /**
129
+ * 원문과 다른 모양으로 다시 쓴 표.
130
+ * @returns {number}
131
+ */
132
+ get rewrittenTable() {
133
+ const ret = wasm.__wbg_get_repairs_rewrittenTable(this.__wbg_ptr);
134
+ return ret >>> 0;
135
+ }
136
+ /**
137
+ * 벗긴 원문 HTML(태그·주석·줄바꿈으로 바꾼 `<br>`).
138
+ * @returns {number}
139
+ */
140
+ get strippedHtml() {
141
+ const ret = wasm.__wbg_get_repairs_strippedHtml(this.__wbg_ptr);
142
+ return ret >>> 0;
143
+ }
144
+ /**
145
+ * 마커 대신 태그로 낸 강조(GitHub 의 `<strong>`).
146
+ * @returns {number}
147
+ */
148
+ get tagEmphasis() {
149
+ const ret = wasm.__wbg_get_repairs_tagEmphasis(this.__wbg_ptr);
150
+ return ret >>> 0;
151
+ }
104
152
  /**
105
153
  * 블록이 끝나도록 안 닫혀서 닫아 준 강조.
106
154
  * @param {number} arg0
@@ -115,6 +163,13 @@ class Repairs {
115
163
  set closedFence(arg0) {
116
164
  wasm.__wbg_set_repairs_closedFence(this.__wbg_ptr, arg0);
117
165
  }
166
+ /**
167
+ * 다른 표기로 바꿔 쓴 강조 마커와 `<url|텍스트>` 링크.
168
+ * @param {number} arg0
169
+ */
170
+ set convertedMarker(arg0) {
171
+ wasm.__wbg_set_repairs_convertedMarker(this.__wbg_ptr, arg0);
172
+ }
118
173
  /**
119
174
  * 짝 잃은 채 버린 `**`.
120
175
  * @param {number} arg0
@@ -122,6 +177,13 @@ class Repairs {
122
177
  set droppedMarker(arg0) {
123
178
  wasm.__wbg_set_repairs_droppedMarker(this.__wbg_ptr, arg0);
124
179
  }
180
+ /**
181
+ * 채널이 구문으로 읽을 글자를 탈출한 수(GitHub 의 `\~`·`\<`·`\*`).
182
+ * @param {number} arg0
183
+ */
184
+ set escapedChar(arg0) {
185
+ wasm.__wbg_set_repairs_escapedChar(this.__wbg_ptr, arg0);
186
+ }
125
187
  /**
126
188
  * 짝이 없어 글자로 되돌린 백틱 런.
127
189
  * @param {number} arg0
@@ -129,6 +191,34 @@ class Repairs {
129
191
  set revertedCodeSpan(arg0) {
130
192
  wasm.__wbg_set_repairs_revertedCodeSpan(this.__wbg_ptr, arg0);
131
193
  }
194
+ /**
195
+ * 다른 기호로 바꿔 쓴 목록 불릿.
196
+ * @param {number} arg0
197
+ */
198
+ set rewrittenBullet(arg0) {
199
+ wasm.__wbg_set_repairs_rewrittenBullet(this.__wbg_ptr, arg0);
200
+ }
201
+ /**
202
+ * 원문과 다른 모양으로 다시 쓴 표.
203
+ * @param {number} arg0
204
+ */
205
+ set rewrittenTable(arg0) {
206
+ wasm.__wbg_set_repairs_rewrittenTable(this.__wbg_ptr, arg0);
207
+ }
208
+ /**
209
+ * 벗긴 원문 HTML(태그·주석·줄바꿈으로 바꾼 `<br>`).
210
+ * @param {number} arg0
211
+ */
212
+ set strippedHtml(arg0) {
213
+ wasm.__wbg_set_repairs_strippedHtml(this.__wbg_ptr, arg0);
214
+ }
215
+ /**
216
+ * 마커 대신 태그로 낸 강조(GitHub 의 `<strong>`).
217
+ * @param {number} arg0
218
+ */
219
+ set tagEmphasis(arg0) {
220
+ wasm.__wbg_set_repairs_tagEmphasis(this.__wbg_ptr, arg0);
221
+ }
132
222
  }
133
223
  if (Symbol.dispose) Repairs.prototype[Symbol.dispose] = Repairs.prototype.free;
134
224
  exports.Repairs = Repairs;
@@ -221,6 +311,35 @@ class Streamer {
221
311
  wasm.__wbindgen_add_to_stack_pointer(16);
222
312
  }
223
313
  }
314
+ /**
315
+ * **지금 입력이 끝났다면 확정분 뒤에 붙을 꼬리.** `closeOpen` 과 같은 자리에 들어가지만
316
+ * 붙들고 있던 것(열린 강조, 표 행, 코드 스팬)까지 그린다. 누적본을 통째로 다시 그리는 쪽의
317
+ * 기본값이다. 추측이라 뒤 조각이 모양을 바꿀 수 있다 — 끝난 뒤 `revised()` 로 본다.
318
+ *
319
+ * ```js
320
+ * acc += s.push(chunk);
321
+ * await edit(acc + s.preview()); // 화면을 그릴 때만 부른다(열린 블록만큼 든다)
322
+ * acc += s.finish();
323
+ * if (s.revised()) await edit(acc); // 마지막 화면이 곧 완성본이면 건너뛴다
324
+ * ```
325
+ * @returns {string}
326
+ */
327
+ preview() {
328
+ let deferred1_0;
329
+ let deferred1_1;
330
+ try {
331
+ const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
332
+ wasm.streamer_preview(retptr, this.__wbg_ptr);
333
+ var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true);
334
+ var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true);
335
+ deferred1_0 = r0;
336
+ deferred1_1 = r1;
337
+ return getStringFromWasm0(r0, r1);
338
+ } finally {
339
+ wasm.__wbindgen_add_to_stack_pointer(16);
340
+ wasm.__wbindgen_export3(deferred1_0, deferred1_1, 1);
341
+ }
342
+ }
224
343
  /**
225
344
  * @param {string} chunk
226
345
  * @returns {string}
@@ -251,6 +370,14 @@ class Streamer {
251
370
  const ret = wasm.streamer_repairs(this.__wbg_ptr);
252
371
  return Repairs.__wrap(ret);
253
372
  }
373
+ /**
374
+ * 완성본이 마지막 `preview()` 와 다른가 — `finish` 뒤에 본다.
375
+ * @returns {boolean}
376
+ */
377
+ revised() {
378
+ const ret = wasm.streamer_revised(this.__wbg_ptr);
379
+ return ret !== 0;
380
+ }
254
381
  }
255
382
  if (Symbol.dispose) Streamer.prototype[Symbol.dispose] = Streamer.prototype.free;
256
383
  exports.Streamer = Streamer;
@@ -345,16 +472,54 @@ function __wbg_get_imports() {
345
472
  const ret = Error(getStringFromWasm0(arg0, arg1));
346
473
  return addHeapObject(ret);
347
474
  },
475
+ __wbg___wbindgen_string_get_92ab86bb19cbc12f: function(arg0, arg1) {
476
+ const obj = getObject(arg1);
477
+ const ret = typeof(obj) === 'string' ? obj : undefined;
478
+ var ptr1 = isLikeNone(ret) ? 0 : passStringToWasm0(ret, wasm.__wbindgen_export, wasm.__wbindgen_export2);
479
+ var len1 = WASM_VECTOR_LEN;
480
+ getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true);
481
+ getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true);
482
+ },
348
483
  __wbg___wbindgen_throw_5d9e815e6fdf150f: function(arg0, arg1) {
349
484
  throw new Error(getStringFromWasm0(arg0, arg1));
350
485
  },
351
- __wbg_from_5ceccc568e2b01b6: function(arg0, arg1) {
486
+ __wbg_from_6a18a824ab73bfc5: function(arg0, arg1) {
352
487
  const ret = getObject(arg1).from;
353
488
  var ptr1 = isLikeNone(ret) ? 0 : passStringToWasm0(ret, wasm.__wbindgen_export, wasm.__wbindgen_export2);
354
489
  var len1 = WASM_VECTOR_LEN;
355
490
  getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true);
356
491
  getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true);
357
492
  },
493
+ __wbg_html_a628fe022a5daeaf: function(arg0) {
494
+ const ret = getObject(arg0).html;
495
+ return isLikeNone(ret) ? 0 : addHeapObject(ret);
496
+ },
497
+ __wbg_images_ff5cc833afad2dcf: function(arg0, arg1) {
498
+ const ret = getObject(arg1).images;
499
+ var ptr1 = isLikeNone(ret) ? 0 : passStringToWasm0(ret, wasm.__wbindgen_export, wasm.__wbindgen_export2);
500
+ var len1 = WASM_VECTOR_LEN;
501
+ getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true);
502
+ getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true);
503
+ },
504
+ __wbg_limit_cce570f9381001ea: function(arg0, arg1) {
505
+ const ret = getObject(arg1).limit;
506
+ getDataViewMemory0().setFloat64(arg0 + 8 * 1, isLikeNone(ret) ? 0 : ret, true);
507
+ getDataViewMemory0().setInt32(arg0 + 4 * 0, !isLikeNone(ret), true);
508
+ },
509
+ __wbg_lineBreaks_aa928a165d0a7659: function(arg0, arg1) {
510
+ const ret = getObject(arg1).lineBreaks;
511
+ var ptr1 = isLikeNone(ret) ? 0 : passStringToWasm0(ret, wasm.__wbindgen_export, wasm.__wbindgen_export2);
512
+ var len1 = WASM_VECTOR_LEN;
513
+ getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true);
514
+ getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true);
515
+ },
516
+ __wbg_schemes_429ccca2fff29ed9: function(arg0, arg1) {
517
+ const ret = getObject(arg1).schemes;
518
+ var ptr1 = isLikeNone(ret) ? 0 : passArrayJsValueToWasm0(ret, wasm.__wbindgen_export);
519
+ var len1 = WASM_VECTOR_LEN;
520
+ getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true);
521
+ getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true);
522
+ },
358
523
  __wbindgen_generic_0000000000000001: function(arg0, arg1) {
359
524
  // Cast intrinsic for `Ref(String) -> Externref`.
360
525
  const ret = getStringFromWasm0(arg0, arg1);
@@ -436,6 +601,16 @@ function isLikeNone(x) {
436
601
  return x === undefined || x === null;
437
602
  }
438
603
 
604
+ function passArrayJsValueToWasm0(array, malloc) {
605
+ const ptr = malloc(array.length * 4, 4) >>> 0;
606
+ const mem = getDataViewMemory0();
607
+ for (let i = 0; i < array.length; i++) {
608
+ mem.setUint32(ptr + 4 * i, addHeapObject(array[i]), true);
609
+ }
610
+ WASM_VECTOR_LEN = array.length;
611
+ return ptr;
612
+ }
613
+
439
614
  function passStringToWasm0(arg, malloc, realloc) {
440
615
  if (realloc === undefined) {
441
616
  const buf = cachedTextEncoder.encode(arg);
Binary file
@@ -3,14 +3,26 @@
3
3
  export const memory: WebAssembly.Memory;
4
4
  export const __wbg_get_repairs_closedEmphasis: (a: number) => number;
5
5
  export const __wbg_get_repairs_closedFence: (a: number) => number;
6
+ export const __wbg_get_repairs_convertedMarker: (a: number) => number;
6
7
  export const __wbg_get_repairs_droppedMarker: (a: number) => number;
8
+ export const __wbg_get_repairs_escapedChar: (a: number) => number;
7
9
  export const __wbg_get_repairs_revertedCodeSpan: (a: number) => number;
10
+ export const __wbg_get_repairs_rewrittenBullet: (a: number) => number;
11
+ export const __wbg_get_repairs_rewrittenTable: (a: number) => number;
12
+ export const __wbg_get_repairs_strippedHtml: (a: number) => number;
13
+ export const __wbg_get_repairs_tagEmphasis: (a: number) => number;
8
14
  export const __wbg_rendered_free: (a: number, b: number) => void;
9
15
  export const __wbg_repairs_free: (a: number, b: number) => void;
10
16
  export const __wbg_set_repairs_closedEmphasis: (a: number, b: number) => void;
11
17
  export const __wbg_set_repairs_closedFence: (a: number, b: number) => void;
18
+ export const __wbg_set_repairs_convertedMarker: (a: number, b: number) => void;
12
19
  export const __wbg_set_repairs_droppedMarker: (a: number, b: number) => void;
20
+ export const __wbg_set_repairs_escapedChar: (a: number, b: number) => void;
13
21
  export const __wbg_set_repairs_revertedCodeSpan: (a: number, b: number) => void;
22
+ export const __wbg_set_repairs_rewrittenBullet: (a: number, b: number) => void;
23
+ export const __wbg_set_repairs_rewrittenTable: (a: number, b: number) => void;
24
+ export const __wbg_set_repairs_strippedHtml: (a: number, b: number) => void;
25
+ export const __wbg_set_repairs_tagEmphasis: (a: number, b: number) => void;
14
26
  export const __wbg_streamer_free: (a: number, b: number) => void;
15
27
  export const limit: (a: number, b: number, c: number) => void;
16
28
  export const render: (a: number, b: number, c: number, d: number, e: number, f: number) => void;
@@ -20,8 +32,10 @@ export const rendered_repairs: (a: number) => number;
20
32
  export const streamer_closeOpen: (a: number, b: number) => void;
21
33
  export const streamer_finish: (a: number, b: number) => void;
22
34
  export const streamer_new: (a: number, b: number, c: number, d: number) => void;
35
+ export const streamer_preview: (a: number, b: number) => void;
23
36
  export const streamer_push: (a: number, b: number, c: number, d: number) => void;
24
37
  export const streamer_repairs: (a: number) => number;
38
+ export const streamer_revised: (a: number) => number;
25
39
  export const __wbindgen_export: (a: number, b: number) => number;
26
40
  export const __wbindgen_export2: (a: number, b: number, c: number, d: number) => number;
27
41
  export const __wbindgen_add_to_stack_pointer: (a: number) => number;
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "@minjun0219/mdwire",
3
- "version": "0.1.7",
4
- "description": "Streaming Markdown renderer for chat channels. Telegram HTML, Slack markdown_text, plain text.",
3
+ "version": "0.1.9",
4
+ "description": "Streaming Markdown renderer for chat channels and the browser. Telegram HTML, Slack markdown_text, GitHub, HTML, React.",
5
5
  "license": "MIT",
6
6
  "repository": { "type": "git", "url": "https://github.com/minjun0219/mdwire" },
7
- "homepage": "https://github.com/minjun0219/mdwire",
8
- "keywords": ["markdown", "telegram", "slack", "streaming", "llm", "wasm"],
7
+ "homepage": "https://mdwire.minjun.dev",
8
+ "keywords": ["markdown", "telegram", "slack", "github", "react", "streaming", "llm", "wasm"],
9
9
  "type": "module",
10
10
  "main": "./node/mdwire.js",
11
11
  "types": "./bundler/mdwire.d.ts",
@@ -14,9 +14,13 @@
14
14
  "types": "./bundler/mdwire.d.ts",
15
15
  "node": "./node/mdwire.js",
16
16
  "default": "./bundler/mdwire.js"
17
- }
17
+ },
18
+ "./events": { "types": "./js/events.d.ts", "default": "./js/events.js" },
19
+ "./react": { "types": "./js/react.d.ts", "default": "./js/react.js" }
18
20
  },
21
+ "peerDependencies": { "react": ">=18" },
22
+ "peerDependenciesMeta": { "react": { "optional": true } },
19
23
  "sideEffects": ["./bundler/mdwire.js"],
20
- "files": ["bundler", "node"],
24
+ "files": ["bundler", "node", "js"],
21
25
  "publishConfig": { "access": "public" }
22
26
  }