@minjun0219/mdwire 0.1.6 → 0.1.8
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 +40 -10
- package/bundler/mdwire.d.ts +28 -0
- package/bundler/mdwire_bg.js +86 -1
- package/bundler/mdwire_bg.wasm +0 -0
- package/bundler/mdwire_bg.wasm.d.ts +2 -0
- package/js/events.d.ts +20 -0
- package/js/events.js +70 -0
- package/js/react.d.ts +43 -0
- package/js/react.js +154 -0
- package/node/mdwire.d.ts +28 -0
- package/node/mdwire.js +86 -1
- package/node/mdwire_bg.wasm +0 -0
- package/node/mdwire_bg.wasm.d.ts +2 -0
- package/package.json +9 -5
package/README.md
CHANGED
|
@@ -9,8 +9,9 @@ escaping rules, and its own length limit. Existing converters assume the input i
|
|
|
9
9
|
well-formed CommonMark and target one channel at a time. Neither assumption holds for
|
|
10
10
|
agent output.
|
|
11
11
|
|
|
12
|
-
**Status: v0.1.
|
|
13
|
-
|
|
12
|
+
**Status: v0.1.8.** Normalizing, rendering, splitting and streaming work for five
|
|
13
|
+
targets — Telegram HTML, Slack `markdown_text`, GitHub comments (GFM), plain text, and HTML for
|
|
14
|
+
the browser — from a Rust core, a CLI,
|
|
14
15
|
an npm package (WASM), and a Go port. See `SPEC.md` for what is in v0.1 and what was
|
|
15
16
|
deliberately deferred. `SPEC.md` and `DESIGN.md` are written in Korean.
|
|
16
17
|
|
|
@@ -23,7 +24,14 @@ LLM markdown → normalize → render for channel → split safely → s
|
|
|
23
24
|
1. **Normalize.** Agent output is not well-formed. Unpaired `**`, emphasis that spans a
|
|
24
25
|
line break in wrapped prose, unclosed code fences. Repair before rendering.
|
|
25
26
|
2. **Render.** Emit the syntax the channel actually accepts. Telegram HTML allows nine
|
|
26
|
-
tags; Slack `markdown_text` takes standard Markdown directly.
|
|
27
|
+
tags; Slack `markdown_text` takes standard Markdown directly. GitHub takes it too, but
|
|
28
|
+
reads a lone `~` as strikethrough and `<T>` as an HTML tag — so a `~` or `<` meant as a
|
|
29
|
+
character goes out escaped (`\~`, `\<`), and emphasis GFM would not close — `**(a)**`
|
|
30
|
+
followed directly by a Korean particle — goes out as `<strong>`. For the browser, `html` draws
|
|
31
|
+
blocks as tags too and is safe to set as `innerHTML`: text is escaped, inline tags from the
|
|
32
|
+
source keep no attributes, and only `http(s)`/`mailto` links become `<a>` — line breaks,
|
|
33
|
+
images and allowed schemes are options. While streaming,
|
|
34
|
+
the accumulated output plus `preview()` (or `closeOpen()`) is always balanced HTML.
|
|
27
35
|
3. **Split.** Respect the channel's limit — and never cut through markup. This also
|
|
28
36
|
covers streaming: a chunk boundary must not land inside `**bold**`.
|
|
29
37
|
|
|
@@ -38,6 +46,7 @@ markers — so you can log how often the model breaks its own formatting.
|
|
|
38
46
|
```sh
|
|
39
47
|
cat agent-output.md | mdwire --channel telegram-html # parts separated by NUL
|
|
40
48
|
cat agent-output.md | mdwire --channel slack-markdown --stream # emit as it arrives
|
|
49
|
+
cat agent-output.md | mdwire --channel plain --limit 4096 # plain fallback into Telegram
|
|
41
50
|
|
|
42
51
|
# The agent wrote Slack's legacy mrkdwn (*bold*, ~strike~)? Say so. --report prints what
|
|
43
52
|
# the normalizer fixed (unclosed emphasis, unclosed fence, …) as one JSON line on stderr.
|
|
@@ -58,17 +67,19 @@ import { render, renderWithReport, Streamer } from "@minjun0219/mdwire"; // np
|
|
|
58
67
|
const parts = render(markdown, "telegram-html");
|
|
59
68
|
const { repairs } = renderWithReport(markdown, "slack-markdown", { from: "slack-mrkdwn" });
|
|
60
69
|
|
|
61
|
-
// A channel that rewrites the whole message (Telegram edit): send acc plus the
|
|
62
|
-
//
|
|
70
|
+
// A channel that rewrites the whole message (Telegram edit): send acc plus the preview —
|
|
71
|
+
// what is still held (open bold, table rows, a code span) drawn as if the input ended here.
|
|
72
|
+
// Keep acc itself untouched. After finish, skip the last edit if nothing changed.
|
|
63
73
|
const s = new Streamer("telegram-html");
|
|
64
74
|
let acc = "";
|
|
65
75
|
for await (const chunk of tokens) {
|
|
66
76
|
acc += s.push(chunk);
|
|
67
|
-
await edit(acc + s.
|
|
77
|
+
await edit(acc + s.preview());
|
|
68
78
|
}
|
|
69
79
|
acc += s.finish();
|
|
80
|
+
if (s.revised()) await edit(acc);
|
|
70
81
|
|
|
71
|
-
// An append-only channel (Slack appendStream): send each piece as is — never
|
|
82
|
+
// An append-only channel (Slack appendStream): send each piece as is — never preview.
|
|
72
83
|
const t = new Streamer("slack-markdown");
|
|
73
84
|
for await (const chunk of tokens) {
|
|
74
85
|
const piece = t.push(chunk);
|
|
@@ -77,6 +88,25 @@ for await (const chunk of tokens) {
|
|
|
77
88
|
await append(t.finish());
|
|
78
89
|
```
|
|
79
90
|
|
|
91
|
+
In React, `@minjun0219/mdwire/react` builds elements with `createElement` — no
|
|
92
|
+
`innerHTML`. Escaping, the tag set and link schemes are decided once, in the core's `html`
|
|
93
|
+
channel; you choose which component draws each tag. `@minjun0219/mdwire/events` gives the
|
|
94
|
+
same output as an `open` / `text` / `close` event list for other frameworks.
|
|
95
|
+
|
|
96
|
+
```jsx
|
|
97
|
+
import { Markdown, useMarkdownStream } from "@minjun0219/mdwire/react";
|
|
98
|
+
|
|
99
|
+
<Markdown text={answer} components={{ a: RouterLink }} /> // a finished answer
|
|
100
|
+
const { elements, push, finish } = useMarkdownStream(); // streaming: push(token), finish()
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
The hook draws held content early by default; `useMarkdownStream({ eager: false })` shows only
|
|
104
|
+
what is final, and `onSettled(html, revised)` tells you whether the finished text differs from
|
|
105
|
+
the last frame.
|
|
106
|
+
|
|
107
|
+
[`examples/react-streaming`](examples/react-streaming) streams one answer into react-markdown, Streamdown,
|
|
108
|
+
mdwire in front of Streamdown, and mdwire side by side. Measured numbers are in `DESIGN.md`.
|
|
109
|
+
|
|
80
110
|
**Append-only contract.** What `push` returns is final — a later chunk never rewrites it —
|
|
81
111
|
and `finish` only appends the tail. So the pieces concatenated equal a one-shot `render`,
|
|
82
112
|
whatever the chunk size (unless the document is long enough to be split into parts).
|
|
@@ -128,11 +158,11 @@ Every release also carries its own artifacts, if you would rather not go through
|
|
|
128
158
|
|
|
129
159
|
```sh
|
|
130
160
|
# npm package (works under a bundler and in plain Node)
|
|
131
|
-
npm install https://github.com/minjun0219/mdwire/releases/download/v0.1.
|
|
161
|
+
npm install https://github.com/minjun0219/mdwire/releases/download/v0.1.8/mdwire-0.1.8.tgz
|
|
132
162
|
|
|
133
163
|
# CLI binary — macOS (Apple silicon) or Linux (x86_64)
|
|
134
|
-
curl -L https://github.com/minjun0219/mdwire/releases/download/v0.1.
|
|
135
|
-
curl -L https://github.com/minjun0219/mdwire/releases/download/v0.1.
|
|
164
|
+
curl -L https://github.com/minjun0219/mdwire/releases/download/v0.1.8/mdwire-v0.1.8-aarch64-apple-darwin.tar.gz | tar xz
|
|
165
|
+
curl -L https://github.com/minjun0219/mdwire/releases/download/v0.1.8/mdwire-v0.1.8-x86_64-unknown-linux-gnu.tar.gz | tar xz
|
|
136
166
|
```
|
|
137
167
|
|
|
138
168
|
The release notes list a SHA-256 for every artifact — pin to it when installing by URL.
|
package/bundler/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
|
|
|
@@ -77,11 +88,28 @@ export class Streamer {
|
|
|
77
88
|
*/
|
|
78
89
|
finish(): string;
|
|
79
90
|
constructor(channel: string, options?: RenderOptions | null);
|
|
91
|
+
/**
|
|
92
|
+
* **지금 입력이 끝났다면 확정분 뒤에 붙을 꼬리.** `closeOpen` 과 같은 자리에 들어가지만
|
|
93
|
+
* 붙들고 있던 것(열린 강조, 표 행, 코드 스팬)까지 그린다. 누적본을 통째로 다시 그리는 쪽의
|
|
94
|
+
* 기본값이다. 추측이라 뒤 조각이 모양을 바꿀 수 있다 — 끝난 뒤 `revised()` 로 본다.
|
|
95
|
+
*
|
|
96
|
+
* ```js
|
|
97
|
+
* acc += s.push(chunk);
|
|
98
|
+
* await edit(acc + s.preview()); // 화면을 그릴 때만 부른다(열린 블록만큼 든다)
|
|
99
|
+
* acc += s.finish();
|
|
100
|
+
* if (s.revised()) await edit(acc); // 마지막 화면이 곧 완성본이면 건너뛴다
|
|
101
|
+
* ```
|
|
102
|
+
*/
|
|
103
|
+
preview(): string;
|
|
80
104
|
push(chunk: string): string;
|
|
81
105
|
/**
|
|
82
106
|
* 지금까지 정규화가 고친 것. `finish` 뒤에 보면 문서 전체의 값이다.
|
|
83
107
|
*/
|
|
84
108
|
repairs(): Repairs;
|
|
109
|
+
/**
|
|
110
|
+
* 완성본이 마지막 `preview()` 와 다른가 — `finish` 뒤에 본다.
|
|
111
|
+
*/
|
|
112
|
+
revised(): boolean;
|
|
85
113
|
}
|
|
86
114
|
|
|
87
115
|
/**
|
package/bundler/mdwire_bg.js
CHANGED
|
@@ -217,6 +217,35 @@ export class Streamer {
|
|
|
217
217
|
wasm.__wbindgen_add_to_stack_pointer(16);
|
|
218
218
|
}
|
|
219
219
|
}
|
|
220
|
+
/**
|
|
221
|
+
* **지금 입력이 끝났다면 확정분 뒤에 붙을 꼬리.** `closeOpen` 과 같은 자리에 들어가지만
|
|
222
|
+
* 붙들고 있던 것(열린 강조, 표 행, 코드 스팬)까지 그린다. 누적본을 통째로 다시 그리는 쪽의
|
|
223
|
+
* 기본값이다. 추측이라 뒤 조각이 모양을 바꿀 수 있다 — 끝난 뒤 `revised()` 로 본다.
|
|
224
|
+
*
|
|
225
|
+
* ```js
|
|
226
|
+
* acc += s.push(chunk);
|
|
227
|
+
* await edit(acc + s.preview()); // 화면을 그릴 때만 부른다(열린 블록만큼 든다)
|
|
228
|
+
* acc += s.finish();
|
|
229
|
+
* if (s.revised()) await edit(acc); // 마지막 화면이 곧 완성본이면 건너뛴다
|
|
230
|
+
* ```
|
|
231
|
+
* @returns {string}
|
|
232
|
+
*/
|
|
233
|
+
preview() {
|
|
234
|
+
let deferred1_0;
|
|
235
|
+
let deferred1_1;
|
|
236
|
+
try {
|
|
237
|
+
const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
|
|
238
|
+
wasm.streamer_preview(retptr, this.__wbg_ptr);
|
|
239
|
+
var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true);
|
|
240
|
+
var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true);
|
|
241
|
+
deferred1_0 = r0;
|
|
242
|
+
deferred1_1 = r1;
|
|
243
|
+
return getStringFromWasm0(r0, r1);
|
|
244
|
+
} finally {
|
|
245
|
+
wasm.__wbindgen_add_to_stack_pointer(16);
|
|
246
|
+
wasm.__wbindgen_export3(deferred1_0, deferred1_1, 1);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
220
249
|
/**
|
|
221
250
|
* @param {string} chunk
|
|
222
251
|
* @returns {string}
|
|
@@ -247,6 +276,14 @@ export class Streamer {
|
|
|
247
276
|
const ret = wasm.streamer_repairs(this.__wbg_ptr);
|
|
248
277
|
return Repairs.__wrap(ret);
|
|
249
278
|
}
|
|
279
|
+
/**
|
|
280
|
+
* 완성본이 마지막 `preview()` 와 다른가 — `finish` 뒤에 본다.
|
|
281
|
+
* @returns {boolean}
|
|
282
|
+
*/
|
|
283
|
+
revised() {
|
|
284
|
+
const ret = wasm.streamer_revised(this.__wbg_ptr);
|
|
285
|
+
return ret !== 0;
|
|
286
|
+
}
|
|
250
287
|
}
|
|
251
288
|
if (Symbol.dispose) Streamer.prototype[Symbol.dispose] = Streamer.prototype.free;
|
|
252
289
|
|
|
@@ -334,16 +371,54 @@ export function __wbg_Error_67e7344beaa85059(arg0, arg1) {
|
|
|
334
371
|
const ret = Error(getStringFromWasm0(arg0, arg1));
|
|
335
372
|
return addHeapObject(ret);
|
|
336
373
|
}
|
|
374
|
+
export function __wbg___wbindgen_string_get_92ab86bb19cbc12f(arg0, arg1) {
|
|
375
|
+
const obj = getObject(arg1);
|
|
376
|
+
const ret = typeof(obj) === 'string' ? obj : undefined;
|
|
377
|
+
var ptr1 = isLikeNone(ret) ? 0 : passStringToWasm0(ret, wasm.__wbindgen_export, wasm.__wbindgen_export2);
|
|
378
|
+
var len1 = WASM_VECTOR_LEN;
|
|
379
|
+
getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true);
|
|
380
|
+
getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true);
|
|
381
|
+
}
|
|
337
382
|
export function __wbg___wbindgen_throw_5d9e815e6fdf150f(arg0, arg1) {
|
|
338
383
|
throw new Error(getStringFromWasm0(arg0, arg1));
|
|
339
384
|
}
|
|
340
|
-
export function
|
|
385
|
+
export function __wbg_from_5fbc1c43b212eab8(arg0, arg1) {
|
|
341
386
|
const ret = getObject(arg1).from;
|
|
342
387
|
var ptr1 = isLikeNone(ret) ? 0 : passStringToWasm0(ret, wasm.__wbindgen_export, wasm.__wbindgen_export2);
|
|
343
388
|
var len1 = WASM_VECTOR_LEN;
|
|
344
389
|
getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true);
|
|
345
390
|
getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true);
|
|
346
391
|
}
|
|
392
|
+
export function __wbg_html_90d0c88b318a2996(arg0) {
|
|
393
|
+
const ret = getObject(arg0).html;
|
|
394
|
+
return isLikeNone(ret) ? 0 : addHeapObject(ret);
|
|
395
|
+
}
|
|
396
|
+
export function __wbg_images_61d0abc71a1a31d9(arg0, arg1) {
|
|
397
|
+
const ret = getObject(arg1).images;
|
|
398
|
+
var ptr1 = isLikeNone(ret) ? 0 : passStringToWasm0(ret, wasm.__wbindgen_export, wasm.__wbindgen_export2);
|
|
399
|
+
var len1 = WASM_VECTOR_LEN;
|
|
400
|
+
getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true);
|
|
401
|
+
getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true);
|
|
402
|
+
}
|
|
403
|
+
export function __wbg_limit_04422401e3a1f949(arg0, arg1) {
|
|
404
|
+
const ret = getObject(arg1).limit;
|
|
405
|
+
getDataViewMemory0().setFloat64(arg0 + 8 * 1, isLikeNone(ret) ? 0 : ret, true);
|
|
406
|
+
getDataViewMemory0().setInt32(arg0 + 4 * 0, !isLikeNone(ret), true);
|
|
407
|
+
}
|
|
408
|
+
export function __wbg_lineBreaks_05a2d00e6ab123b1(arg0, arg1) {
|
|
409
|
+
const ret = getObject(arg1).lineBreaks;
|
|
410
|
+
var ptr1 = isLikeNone(ret) ? 0 : passStringToWasm0(ret, wasm.__wbindgen_export, wasm.__wbindgen_export2);
|
|
411
|
+
var len1 = WASM_VECTOR_LEN;
|
|
412
|
+
getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true);
|
|
413
|
+
getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true);
|
|
414
|
+
}
|
|
415
|
+
export function __wbg_schemes_224d41c59d295a23(arg0, arg1) {
|
|
416
|
+
const ret = getObject(arg1).schemes;
|
|
417
|
+
var ptr1 = isLikeNone(ret) ? 0 : passArrayJsValueToWasm0(ret, wasm.__wbindgen_export);
|
|
418
|
+
var len1 = WASM_VECTOR_LEN;
|
|
419
|
+
getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true);
|
|
420
|
+
getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true);
|
|
421
|
+
}
|
|
347
422
|
export function __wbindgen_generic_0000000000000001(arg0, arg1) {
|
|
348
423
|
// Cast intrinsic for `Ref(String) -> Externref`.
|
|
349
424
|
const ret = getStringFromWasm0(arg0, arg1);
|
|
@@ -418,6 +493,16 @@ function isLikeNone(x) {
|
|
|
418
493
|
return x === undefined || x === null;
|
|
419
494
|
}
|
|
420
495
|
|
|
496
|
+
function passArrayJsValueToWasm0(array, malloc) {
|
|
497
|
+
const ptr = malloc(array.length * 4, 4) >>> 0;
|
|
498
|
+
const mem = getDataViewMemory0();
|
|
499
|
+
for (let i = 0; i < array.length; i++) {
|
|
500
|
+
mem.setUint32(ptr + 4 * i, addHeapObject(array[i]), true);
|
|
501
|
+
}
|
|
502
|
+
WASM_VECTOR_LEN = array.length;
|
|
503
|
+
return ptr;
|
|
504
|
+
}
|
|
505
|
+
|
|
421
506
|
function passStringToWasm0(arg, malloc, realloc) {
|
|
422
507
|
if (realloc === undefined) {
|
|
423
508
|
const buf = cachedTextEncoder.encode(arg);
|
package/bundler/mdwire_bg.wasm
CHANGED
|
Binary file
|
|
@@ -20,8 +20,10 @@ export const rendered_repairs: (a: number) => number;
|
|
|
20
20
|
export const streamer_closeOpen: (a: number, b: number) => void;
|
|
21
21
|
export const streamer_finish: (a: number, b: number) => void;
|
|
22
22
|
export const streamer_new: (a: number, b: number, c: number, d: number) => void;
|
|
23
|
+
export const streamer_preview: (a: number, b: number) => void;
|
|
23
24
|
export const streamer_push: (a: number, b: number, c: number, d: number) => void;
|
|
24
25
|
export const streamer_repairs: (a: number) => number;
|
|
26
|
+
export const streamer_revised: (a: number) => number;
|
|
25
27
|
export const __wbindgen_export: (a: number, b: number) => number;
|
|
26
28
|
export const __wbindgen_export2: (a: number, b: number, c: number, d: number) => number;
|
|
27
29
|
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" | "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", "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
|
|
|
@@ -77,11 +88,28 @@ export class Streamer {
|
|
|
77
88
|
*/
|
|
78
89
|
finish(): string;
|
|
79
90
|
constructor(channel: string, options?: RenderOptions | null);
|
|
91
|
+
/**
|
|
92
|
+
* **지금 입력이 끝났다면 확정분 뒤에 붙을 꼬리.** `closeOpen` 과 같은 자리에 들어가지만
|
|
93
|
+
* 붙들고 있던 것(열린 강조, 표 행, 코드 스팬)까지 그린다. 누적본을 통째로 다시 그리는 쪽의
|
|
94
|
+
* 기본값이다. 추측이라 뒤 조각이 모양을 바꿀 수 있다 — 끝난 뒤 `revised()` 로 본다.
|
|
95
|
+
*
|
|
96
|
+
* ```js
|
|
97
|
+
* acc += s.push(chunk);
|
|
98
|
+
* await edit(acc + s.preview()); // 화면을 그릴 때만 부른다(열린 블록만큼 든다)
|
|
99
|
+
* acc += s.finish();
|
|
100
|
+
* if (s.revised()) await edit(acc); // 마지막 화면이 곧 완성본이면 건너뛴다
|
|
101
|
+
* ```
|
|
102
|
+
*/
|
|
103
|
+
preview(): string;
|
|
80
104
|
push(chunk: string): string;
|
|
81
105
|
/**
|
|
82
106
|
* 지금까지 정규화가 고친 것. `finish` 뒤에 보면 문서 전체의 값이다.
|
|
83
107
|
*/
|
|
84
108
|
repairs(): Repairs;
|
|
109
|
+
/**
|
|
110
|
+
* 완성본이 마지막 `preview()` 와 다른가 — `finish` 뒤에 본다.
|
|
111
|
+
*/
|
|
112
|
+
revised(): boolean;
|
|
85
113
|
}
|
|
86
114
|
|
|
87
115
|
/**
|
package/node/mdwire.js
CHANGED
|
@@ -221,6 +221,35 @@ class Streamer {
|
|
|
221
221
|
wasm.__wbindgen_add_to_stack_pointer(16);
|
|
222
222
|
}
|
|
223
223
|
}
|
|
224
|
+
/**
|
|
225
|
+
* **지금 입력이 끝났다면 확정분 뒤에 붙을 꼬리.** `closeOpen` 과 같은 자리에 들어가지만
|
|
226
|
+
* 붙들고 있던 것(열린 강조, 표 행, 코드 스팬)까지 그린다. 누적본을 통째로 다시 그리는 쪽의
|
|
227
|
+
* 기본값이다. 추측이라 뒤 조각이 모양을 바꿀 수 있다 — 끝난 뒤 `revised()` 로 본다.
|
|
228
|
+
*
|
|
229
|
+
* ```js
|
|
230
|
+
* acc += s.push(chunk);
|
|
231
|
+
* await edit(acc + s.preview()); // 화면을 그릴 때만 부른다(열린 블록만큼 든다)
|
|
232
|
+
* acc += s.finish();
|
|
233
|
+
* if (s.revised()) await edit(acc); // 마지막 화면이 곧 완성본이면 건너뛴다
|
|
234
|
+
* ```
|
|
235
|
+
* @returns {string}
|
|
236
|
+
*/
|
|
237
|
+
preview() {
|
|
238
|
+
let deferred1_0;
|
|
239
|
+
let deferred1_1;
|
|
240
|
+
try {
|
|
241
|
+
const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
|
|
242
|
+
wasm.streamer_preview(retptr, this.__wbg_ptr);
|
|
243
|
+
var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true);
|
|
244
|
+
var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true);
|
|
245
|
+
deferred1_0 = r0;
|
|
246
|
+
deferred1_1 = r1;
|
|
247
|
+
return getStringFromWasm0(r0, r1);
|
|
248
|
+
} finally {
|
|
249
|
+
wasm.__wbindgen_add_to_stack_pointer(16);
|
|
250
|
+
wasm.__wbindgen_export3(deferred1_0, deferred1_1, 1);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
224
253
|
/**
|
|
225
254
|
* @param {string} chunk
|
|
226
255
|
* @returns {string}
|
|
@@ -251,6 +280,14 @@ class Streamer {
|
|
|
251
280
|
const ret = wasm.streamer_repairs(this.__wbg_ptr);
|
|
252
281
|
return Repairs.__wrap(ret);
|
|
253
282
|
}
|
|
283
|
+
/**
|
|
284
|
+
* 완성본이 마지막 `preview()` 와 다른가 — `finish` 뒤에 본다.
|
|
285
|
+
* @returns {boolean}
|
|
286
|
+
*/
|
|
287
|
+
revised() {
|
|
288
|
+
const ret = wasm.streamer_revised(this.__wbg_ptr);
|
|
289
|
+
return ret !== 0;
|
|
290
|
+
}
|
|
254
291
|
}
|
|
255
292
|
if (Symbol.dispose) Streamer.prototype[Symbol.dispose] = Streamer.prototype.free;
|
|
256
293
|
exports.Streamer = Streamer;
|
|
@@ -345,16 +382,54 @@ function __wbg_get_imports() {
|
|
|
345
382
|
const ret = Error(getStringFromWasm0(arg0, arg1));
|
|
346
383
|
return addHeapObject(ret);
|
|
347
384
|
},
|
|
385
|
+
__wbg___wbindgen_string_get_92ab86bb19cbc12f: function(arg0, arg1) {
|
|
386
|
+
const obj = getObject(arg1);
|
|
387
|
+
const ret = typeof(obj) === 'string' ? obj : undefined;
|
|
388
|
+
var ptr1 = isLikeNone(ret) ? 0 : passStringToWasm0(ret, wasm.__wbindgen_export, wasm.__wbindgen_export2);
|
|
389
|
+
var len1 = WASM_VECTOR_LEN;
|
|
390
|
+
getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true);
|
|
391
|
+
getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true);
|
|
392
|
+
},
|
|
348
393
|
__wbg___wbindgen_throw_5d9e815e6fdf150f: function(arg0, arg1) {
|
|
349
394
|
throw new Error(getStringFromWasm0(arg0, arg1));
|
|
350
395
|
},
|
|
351
|
-
|
|
396
|
+
__wbg_from_5fbc1c43b212eab8: function(arg0, arg1) {
|
|
352
397
|
const ret = getObject(arg1).from;
|
|
353
398
|
var ptr1 = isLikeNone(ret) ? 0 : passStringToWasm0(ret, wasm.__wbindgen_export, wasm.__wbindgen_export2);
|
|
354
399
|
var len1 = WASM_VECTOR_LEN;
|
|
355
400
|
getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true);
|
|
356
401
|
getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true);
|
|
357
402
|
},
|
|
403
|
+
__wbg_html_90d0c88b318a2996: function(arg0) {
|
|
404
|
+
const ret = getObject(arg0).html;
|
|
405
|
+
return isLikeNone(ret) ? 0 : addHeapObject(ret);
|
|
406
|
+
},
|
|
407
|
+
__wbg_images_61d0abc71a1a31d9: function(arg0, arg1) {
|
|
408
|
+
const ret = getObject(arg1).images;
|
|
409
|
+
var ptr1 = isLikeNone(ret) ? 0 : passStringToWasm0(ret, wasm.__wbindgen_export, wasm.__wbindgen_export2);
|
|
410
|
+
var len1 = WASM_VECTOR_LEN;
|
|
411
|
+
getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true);
|
|
412
|
+
getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true);
|
|
413
|
+
},
|
|
414
|
+
__wbg_limit_04422401e3a1f949: function(arg0, arg1) {
|
|
415
|
+
const ret = getObject(arg1).limit;
|
|
416
|
+
getDataViewMemory0().setFloat64(arg0 + 8 * 1, isLikeNone(ret) ? 0 : ret, true);
|
|
417
|
+
getDataViewMemory0().setInt32(arg0 + 4 * 0, !isLikeNone(ret), true);
|
|
418
|
+
},
|
|
419
|
+
__wbg_lineBreaks_05a2d00e6ab123b1: function(arg0, arg1) {
|
|
420
|
+
const ret = getObject(arg1).lineBreaks;
|
|
421
|
+
var ptr1 = isLikeNone(ret) ? 0 : passStringToWasm0(ret, wasm.__wbindgen_export, wasm.__wbindgen_export2);
|
|
422
|
+
var len1 = WASM_VECTOR_LEN;
|
|
423
|
+
getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true);
|
|
424
|
+
getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true);
|
|
425
|
+
},
|
|
426
|
+
__wbg_schemes_224d41c59d295a23: function(arg0, arg1) {
|
|
427
|
+
const ret = getObject(arg1).schemes;
|
|
428
|
+
var ptr1 = isLikeNone(ret) ? 0 : passArrayJsValueToWasm0(ret, wasm.__wbindgen_export);
|
|
429
|
+
var len1 = WASM_VECTOR_LEN;
|
|
430
|
+
getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true);
|
|
431
|
+
getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true);
|
|
432
|
+
},
|
|
358
433
|
__wbindgen_generic_0000000000000001: function(arg0, arg1) {
|
|
359
434
|
// Cast intrinsic for `Ref(String) -> Externref`.
|
|
360
435
|
const ret = getStringFromWasm0(arg0, arg1);
|
|
@@ -436,6 +511,16 @@ function isLikeNone(x) {
|
|
|
436
511
|
return x === undefined || x === null;
|
|
437
512
|
}
|
|
438
513
|
|
|
514
|
+
function passArrayJsValueToWasm0(array, malloc) {
|
|
515
|
+
const ptr = malloc(array.length * 4, 4) >>> 0;
|
|
516
|
+
const mem = getDataViewMemory0();
|
|
517
|
+
for (let i = 0; i < array.length; i++) {
|
|
518
|
+
mem.setUint32(ptr + 4 * i, addHeapObject(array[i]), true);
|
|
519
|
+
}
|
|
520
|
+
WASM_VECTOR_LEN = array.length;
|
|
521
|
+
return ptr;
|
|
522
|
+
}
|
|
523
|
+
|
|
439
524
|
function passStringToWasm0(arg, malloc, realloc) {
|
|
440
525
|
if (realloc === undefined) {
|
|
441
526
|
const buf = cachedTextEncoder.encode(arg);
|
package/node/mdwire_bg.wasm
CHANGED
|
Binary file
|
package/node/mdwire_bg.wasm.d.ts
CHANGED
|
@@ -20,8 +20,10 @@ export const rendered_repairs: (a: number) => number;
|
|
|
20
20
|
export const streamer_closeOpen: (a: number, b: number) => void;
|
|
21
21
|
export const streamer_finish: (a: number, b: number) => void;
|
|
22
22
|
export const streamer_new: (a: number, b: number, c: number, d: number) => void;
|
|
23
|
+
export const streamer_preview: (a: number, b: number) => void;
|
|
23
24
|
export const streamer_push: (a: number, b: number, c: number, d: number) => void;
|
|
24
25
|
export const streamer_repairs: (a: number) => number;
|
|
26
|
+
export const streamer_revised: (a: number) => number;
|
|
25
27
|
export const __wbindgen_export: (a: number, b: number) => number;
|
|
26
28
|
export const __wbindgen_export2: (a: number, b: number, c: number, d: number) => number;
|
|
27
29
|
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.
|
|
4
|
-
"description": "Streaming Markdown renderer for chat channels. Telegram HTML, Slack markdown_text,
|
|
3
|
+
"version": "0.1.8",
|
|
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
7
|
"homepage": "https://github.com/minjun0219/mdwire",
|
|
8
|
-
"keywords": ["markdown", "telegram", "slack", "streaming", "llm", "wasm"],
|
|
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
|
}
|