@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 +53 -17
- package/bundler/mdwire.d.ts +53 -1
- package/bundler/mdwire_bg.js +177 -2
- package/bundler/mdwire_bg.wasm +0 -0
- package/bundler/mdwire_bg.wasm.d.ts +14 -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 +53 -1
- package/node/mdwire.js +177 -2
- package/node/mdwire_bg.wasm +0 -0
- package/node/mdwire_bg.wasm.d.ts +14 -0
- package/package.json +10 -6
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.
|
|
13
|
-
|
|
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. **
|
|
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 —
|
|
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,
|
|
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
|
|
62
|
-
//
|
|
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.
|
|
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
|
|
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.
|
|
85
|
-
|
|
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.
|
|
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.
|
|
135
|
-
curl -L https://github.com/minjun0219/mdwire/releases/download/v0.1.
|
|
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.
|
|
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
|
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
|
|
|
@@ -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/bundler/mdwire_bg.js
CHANGED
|
@@ -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
|
|
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);
|
package/bundler/mdwire_bg.wasm
CHANGED
|
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
|
-
|
|
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);
|
package/node/mdwire_bg.wasm
CHANGED
|
Binary file
|
package/node/mdwire_bg.wasm.d.ts
CHANGED
|
@@ -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.
|
|
4
|
-
"description": "Streaming Markdown renderer for chat channels. Telegram HTML, Slack markdown_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://
|
|
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
|
}
|