@milkdown/crepe 7.20.0 → 7.21.1
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/lib/cjs/builder.js +1 -0
- package/lib/cjs/builder.js.map +1 -1
- package/lib/cjs/feature/ai/index.js +1492 -0
- package/lib/cjs/feature/ai/index.js.map +1 -0
- package/lib/cjs/feature/block-edit/index.js +1 -0
- package/lib/cjs/feature/block-edit/index.js.map +1 -1
- package/lib/cjs/feature/code-mirror/index.js +1 -0
- package/lib/cjs/feature/code-mirror/index.js.map +1 -1
- package/lib/cjs/feature/cursor/index.js +1 -0
- package/lib/cjs/feature/cursor/index.js.map +1 -1
- package/lib/cjs/feature/image-block/index.js +1 -0
- package/lib/cjs/feature/image-block/index.js.map +1 -1
- package/lib/cjs/feature/latex/index.js +2 -0
- package/lib/cjs/feature/latex/index.js.map +1 -1
- package/lib/cjs/feature/link-tooltip/index.js +1 -0
- package/lib/cjs/feature/link-tooltip/index.js.map +1 -1
- package/lib/cjs/feature/list-item/index.js +1 -0
- package/lib/cjs/feature/list-item/index.js.map +1 -1
- package/lib/cjs/feature/placeholder/index.js +1 -0
- package/lib/cjs/feature/placeholder/index.js.map +1 -1
- package/lib/cjs/feature/table/index.js +1 -0
- package/lib/cjs/feature/table/index.js.map +1 -1
- package/lib/cjs/feature/toolbar/index.js +488 -3
- package/lib/cjs/feature/toolbar/index.js.map +1 -1
- package/lib/cjs/feature/top-bar/index.js +1 -0
- package/lib/cjs/feature/top-bar/index.js.map +1 -1
- package/lib/cjs/index.js +1424 -25
- package/lib/cjs/index.js.map +1 -1
- package/lib/cjs/llm-providers/anthropic/index.js +152 -0
- package/lib/cjs/llm-providers/anthropic/index.js.map +1 -0
- package/lib/cjs/llm-providers/openai/index.js +143 -0
- package/lib/cjs/llm-providers/openai/index.js.map +1 -0
- package/lib/esm/builder.js +1 -0
- package/lib/esm/builder.js.map +1 -1
- package/lib/esm/feature/ai/index.js +1487 -0
- package/lib/esm/feature/ai/index.js.map +1 -0
- package/lib/esm/feature/block-edit/index.js +1 -0
- package/lib/esm/feature/block-edit/index.js.map +1 -1
- package/lib/esm/feature/code-mirror/index.js +1 -0
- package/lib/esm/feature/code-mirror/index.js.map +1 -1
- package/lib/esm/feature/cursor/index.js +1 -0
- package/lib/esm/feature/cursor/index.js.map +1 -1
- package/lib/esm/feature/image-block/index.js +1 -0
- package/lib/esm/feature/image-block/index.js.map +1 -1
- package/lib/esm/feature/latex/index.js +2 -0
- package/lib/esm/feature/latex/index.js.map +1 -1
- package/lib/esm/feature/link-tooltip/index.js +1 -0
- package/lib/esm/feature/link-tooltip/index.js.map +1 -1
- package/lib/esm/feature/list-item/index.js +1 -0
- package/lib/esm/feature/list-item/index.js.map +1 -1
- package/lib/esm/feature/placeholder/index.js +1 -0
- package/lib/esm/feature/placeholder/index.js.map +1 -1
- package/lib/esm/feature/table/index.js +1 -0
- package/lib/esm/feature/table/index.js.map +1 -1
- package/lib/esm/feature/toolbar/index.js +490 -5
- package/lib/esm/feature/toolbar/index.js.map +1 -1
- package/lib/esm/feature/top-bar/index.js +1 -0
- package/lib/esm/feature/top-bar/index.js.map +1 -1
- package/lib/esm/index.js +1414 -15
- package/lib/esm/index.js.map +1 -1
- package/lib/esm/llm-providers/anthropic/index.js +150 -0
- package/lib/esm/llm-providers/anthropic/index.js.map +1 -0
- package/lib/esm/llm-providers/openai/index.js +141 -0
- package/lib/esm/llm-providers/openai/index.js.map +1 -0
- package/lib/theme/common/ai.css +446 -0
- package/lib/theme/common/code-mirror.css +14 -0
- package/lib/theme/common/diff.css +177 -0
- package/lib/theme/common/style.css +2 -0
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/lib/types/feature/ai/ai.spec.d.ts +2 -0
- package/lib/types/feature/ai/ai.spec.d.ts.map +1 -0
- package/lib/types/feature/ai/commands.d.ts +24 -0
- package/lib/types/feature/ai/commands.d.ts.map +1 -0
- package/lib/types/feature/ai/context.d.ts +4 -0
- package/lib/types/feature/ai/context.d.ts.map +1 -0
- package/lib/types/feature/ai/diff-actions/index.d.ts +12 -0
- package/lib/types/feature/ai/diff-actions/index.d.ts.map +1 -0
- package/lib/types/feature/ai/diff-actions/view.d.ts +21 -0
- package/lib/types/feature/ai/diff-actions/view.d.ts.map +1 -0
- package/lib/types/feature/ai/index.d.ts +7 -0
- package/lib/types/feature/ai/index.d.ts.map +1 -0
- package/lib/types/feature/ai/instruction-tooltip/component.d.ts +26 -0
- package/lib/types/feature/ai/instruction-tooltip/component.d.ts.map +1 -0
- package/lib/types/feature/ai/instruction-tooltip/index.d.ts +17 -0
- package/lib/types/feature/ai/instruction-tooltip/index.d.ts.map +1 -0
- package/lib/types/feature/ai/instruction-tooltip/suggestions.d.ts +50 -0
- package/lib/types/feature/ai/instruction-tooltip/suggestions.d.ts.map +1 -0
- package/lib/types/feature/ai/instruction-tooltip/view.d.ts +19 -0
- package/lib/types/feature/ai/instruction-tooltip/view.d.ts.map +1 -0
- package/lib/types/feature/ai/streaming-indicator.d.ts +9 -0
- package/lib/types/feature/ai/streaming-indicator.d.ts.map +1 -0
- package/lib/types/feature/ai/types.d.ts +58 -0
- package/lib/types/feature/ai/types.d.ts.map +1 -0
- package/lib/types/feature/index.d.ts +4 -1
- package/lib/types/feature/index.d.ts.map +1 -1
- package/lib/types/feature/latex/inline-tooltip/inline-tooltip.spec.d.ts +2 -0
- package/lib/types/feature/latex/inline-tooltip/inline-tooltip.spec.d.ts.map +1 -0
- package/lib/types/feature/latex/inline-tooltip/view.d.ts.map +1 -1
- package/lib/types/feature/loader.d.ts.map +1 -1
- package/lib/types/feature/toolbar/config.d.ts.map +1 -1
- package/lib/types/feature/toolbar/index.d.ts +1 -0
- package/lib/types/feature/toolbar/index.d.ts.map +1 -1
- package/lib/types/icons/ai.d.ts +2 -0
- package/lib/types/icons/ai.d.ts.map +1 -0
- package/lib/types/icons/chevron-left.d.ts +2 -0
- package/lib/types/icons/chevron-left.d.ts.map +1 -0
- package/lib/types/icons/chevron-right.d.ts +2 -0
- package/lib/types/icons/chevron-right.d.ts.map +1 -0
- package/lib/types/icons/enter-key.d.ts +2 -0
- package/lib/types/icons/enter-key.d.ts.map +1 -0
- package/lib/types/icons/grammar-check.d.ts +2 -0
- package/lib/types/icons/grammar-check.d.ts.map +1 -0
- package/lib/types/icons/index.d.ts +11 -0
- package/lib/types/icons/index.d.ts.map +1 -1
- package/lib/types/icons/longer.d.ts +2 -0
- package/lib/types/icons/longer.d.ts.map +1 -0
- package/lib/types/icons/retry.d.ts +2 -0
- package/lib/types/icons/retry.d.ts.map +1 -0
- package/lib/types/icons/send-prompt.d.ts +2 -0
- package/lib/types/icons/send-prompt.d.ts.map +1 -0
- package/lib/types/icons/send.d.ts +2 -0
- package/lib/types/icons/send.d.ts.map +1 -0
- package/lib/types/icons/shorter.d.ts +2 -0
- package/lib/types/icons/shorter.d.ts.map +1 -0
- package/lib/types/icons/translate.d.ts +2 -0
- package/lib/types/icons/translate.d.ts.map +1 -0
- package/lib/types/llm-providers/anthropic/index.d.ts +21 -0
- package/lib/types/llm-providers/anthropic/index.d.ts.map +1 -0
- package/lib/types/llm-providers/openai/index.d.ts +15 -0
- package/lib/types/llm-providers/openai/index.d.ts.map +1 -0
- package/lib/types/llm-providers/providers.spec.d.ts +2 -0
- package/lib/types/llm-providers/providers.spec.d.ts.map +1 -0
- package/lib/types/llm-providers/shared.d.ts +17 -0
- package/lib/types/llm-providers/shared.d.ts.map +1 -0
- package/package.json +18 -2
- package/src/feature/ai/ai.spec.ts +742 -0
- package/src/feature/ai/commands.ts +257 -0
- package/src/feature/ai/context.ts +45 -0
- package/src/feature/ai/diff-actions/index.ts +95 -0
- package/src/feature/ai/diff-actions/view.ts +237 -0
- package/src/feature/ai/index.ts +118 -0
- package/src/feature/ai/instruction-tooltip/component.tsx +414 -0
- package/src/feature/ai/instruction-tooltip/index.ts +101 -0
- package/src/feature/ai/instruction-tooltip/suggestions.ts +249 -0
- package/src/feature/ai/instruction-tooltip/view.ts +159 -0
- package/src/feature/ai/streaming-indicator.ts +183 -0
- package/src/feature/ai/types.ts +178 -0
- package/src/feature/index.ts +8 -2
- package/src/feature/latex/inline-tooltip/inline-tooltip.spec.ts +81 -0
- package/src/feature/latex/inline-tooltip/view.ts +2 -0
- package/src/feature/loader.ts +4 -0
- package/src/feature/toolbar/config.ts +27 -1
- package/src/feature/toolbar/index.ts +1 -0
- package/src/icons/ai.ts +14 -0
- package/src/icons/chevron-left.ts +15 -0
- package/src/icons/chevron-right.ts +15 -0
- package/src/icons/enter-key.ts +13 -0
- package/src/icons/grammar-check.ts +13 -0
- package/src/icons/index.ts +11 -0
- package/src/icons/longer.ts +13 -0
- package/src/icons/retry.ts +13 -0
- package/src/icons/send-prompt.ts +13 -0
- package/src/icons/send.ts +13 -0
- package/src/icons/shorter.ts +13 -0
- package/src/icons/translate.ts +13 -0
- package/src/llm-providers/anthropic/index.ts +133 -0
- package/src/llm-providers/openai/index.ts +110 -0
- package/src/llm-providers/providers.spec.ts +472 -0
- package/src/llm-providers/shared.ts +170 -0
- package/src/theme/common/ai.css +430 -0
- package/src/theme/common/code-mirror.css +14 -0
- package/src/theme/common/diff.css +196 -0
- package/src/theme/common/style.css +2 -0
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const DEFAULT_SYSTEM_PROMPT = `You are a writing assistant embedded in a markdown editor.
|
|
4
|
+
|
|
5
|
+
Rules:
|
|
6
|
+
- Output markdown only. Never wrap your output in code fences (e.g. \`\`\`markdown ... \`\`\`).
|
|
7
|
+
- Never include preambles, explanations, or sign-offs \u2014 output only the edited or generated content itself.
|
|
8
|
+
- Preserve the original markdown structure (headings, lists, links, code blocks) unless the instruction explicitly asks to change it.
|
|
9
|
+
- If a <selection> is provided, return only the replacement for that selection \u2014 do not repeat surrounding document context.
|
|
10
|
+
- If no <selection> is provided, return content to insert at the cursor that flows with the surrounding document.`;
|
|
11
|
+
function buildDefaultUserMessage(context) {
|
|
12
|
+
const parts = [`<document>
|
|
13
|
+
${context.document}
|
|
14
|
+
</document>`];
|
|
15
|
+
if (context.selection) {
|
|
16
|
+
parts.push(`<selection>
|
|
17
|
+
${context.selection}
|
|
18
|
+
</selection>`);
|
|
19
|
+
}
|
|
20
|
+
parts.push(`<instruction>
|
|
21
|
+
${context.instruction}
|
|
22
|
+
</instruction>`);
|
|
23
|
+
return parts.join("\n\n");
|
|
24
|
+
}
|
|
25
|
+
function isBrowserLike() {
|
|
26
|
+
const g = globalThis;
|
|
27
|
+
if (g.document !== void 0) return true;
|
|
28
|
+
if (g.WorkerGlobalScope !== void 0) return true;
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
function assertBrowserSafe(config, providerName) {
|
|
32
|
+
if (!config.apiKey) return;
|
|
33
|
+
if (!isBrowserLike()) return;
|
|
34
|
+
if (config.dangerouslyAllowBrowser) return;
|
|
35
|
+
throw new Error(
|
|
36
|
+
`[${providerName}] Refusing to send your API key from a browser. Direct browser \u2192 provider calls expose the key to every visitor. Either route through your backend (set \`baseURL\` + \`headers\` and omit \`apiKey\`), or set \`dangerouslyAllowBrowser: true\` to acknowledge the risk (recommended only for desktop apps or BYOK setups).`
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
function resolveSystemPrompt(systemPrompt) {
|
|
40
|
+
if (systemPrompt === null) return null;
|
|
41
|
+
return systemPrompt != null ? systemPrompt : DEFAULT_SYSTEM_PROMPT;
|
|
42
|
+
}
|
|
43
|
+
function stripTrailingSlashes(url) {
|
|
44
|
+
let end = url.length;
|
|
45
|
+
while (end > 0 && url.charCodeAt(end - 1) === 47) end--;
|
|
46
|
+
return end === url.length ? url : url.slice(0, end);
|
|
47
|
+
}
|
|
48
|
+
async function* parseSSE(response, signal) {
|
|
49
|
+
if (!response.body) return;
|
|
50
|
+
const reader = response.body.getReader();
|
|
51
|
+
const decoder = new TextDecoder("utf-8");
|
|
52
|
+
let buffer = "";
|
|
53
|
+
try {
|
|
54
|
+
while (true) {
|
|
55
|
+
if (signal.aborted) return;
|
|
56
|
+
const { done, value } = await reader.read();
|
|
57
|
+
if (signal.aborted) return;
|
|
58
|
+
if (done) break;
|
|
59
|
+
buffer += decoder.decode(value, { stream: true });
|
|
60
|
+
let nl;
|
|
61
|
+
while ((nl = buffer.indexOf("\n")) >= 0) {
|
|
62
|
+
const raw = buffer.slice(0, nl);
|
|
63
|
+
buffer = buffer.slice(nl + 1);
|
|
64
|
+
const line = raw.endsWith("\r") ? raw.slice(0, -1) : raw;
|
|
65
|
+
if (line.startsWith("data: ")) {
|
|
66
|
+
yield line.slice(6);
|
|
67
|
+
} else if (line.startsWith("data:")) {
|
|
68
|
+
yield line.slice(5);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
const tail = buffer.endsWith("\r") ? buffer.slice(0, -1) : buffer;
|
|
73
|
+
if (tail.startsWith("data: ")) yield tail.slice(6);
|
|
74
|
+
else if (tail.startsWith("data:")) yield tail.slice(5);
|
|
75
|
+
} finally {
|
|
76
|
+
reader.releaseLock();
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
async function readErrorBody(response, providerName) {
|
|
80
|
+
let body = "";
|
|
81
|
+
try {
|
|
82
|
+
body = await response.text();
|
|
83
|
+
} catch (e) {
|
|
84
|
+
}
|
|
85
|
+
return new Error(
|
|
86
|
+
`[${providerName}] Request failed with status ${response.status}` + (body ? `: ${body.slice(0, 500)}` : "")
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const PROVIDER_NAME = "milkdown/providers/anthropic";
|
|
91
|
+
const DEFAULT_BASE_URL = "https://api.anthropic.com";
|
|
92
|
+
const DEFAULT_ANTHROPIC_VERSION = "2023-06-01";
|
|
93
|
+
const DEFAULT_MAX_TOKENS = 4096;
|
|
94
|
+
function createAnthropicProvider(config) {
|
|
95
|
+
assertBrowserSafe(config, PROVIDER_NAME);
|
|
96
|
+
return async function* anthropicProvider(context, signal) {
|
|
97
|
+
var _a, _b, _c, _d;
|
|
98
|
+
const systemPrompt = resolveSystemPrompt(config.systemPrompt);
|
|
99
|
+
const userMessage = buildDefaultUserMessage(context);
|
|
100
|
+
const built = config.buildMessages ? config.buildMessages(context, { systemPrompt, userMessage }) : {
|
|
101
|
+
system: systemPrompt != null ? systemPrompt : void 0,
|
|
102
|
+
messages: [
|
|
103
|
+
{ role: "user", content: userMessage }
|
|
104
|
+
]
|
|
105
|
+
};
|
|
106
|
+
const baseURL = stripTrailingSlashes((_a = config.baseURL) != null ? _a : DEFAULT_BASE_URL);
|
|
107
|
+
const url = `${baseURL}/v1/messages`;
|
|
108
|
+
const headers = {
|
|
109
|
+
"Content-Type": "application/json",
|
|
110
|
+
"anthropic-version": (_b = config.anthropicVersion) != null ? _b : DEFAULT_ANTHROPIC_VERSION,
|
|
111
|
+
...config.apiKey ? { "x-api-key": config.apiKey } : {},
|
|
112
|
+
// Required for direct browser → api.anthropic.com calls; harmless
|
|
113
|
+
// when proxying through a backend.
|
|
114
|
+
...config.dangerouslyAllowBrowser ? { "anthropic-dangerous-direct-browser-access": "true" } : {},
|
|
115
|
+
...config.headers
|
|
116
|
+
};
|
|
117
|
+
const body = JSON.stringify({
|
|
118
|
+
model: config.model,
|
|
119
|
+
max_tokens: (_c = config.maxTokens) != null ? _c : DEFAULT_MAX_TOKENS,
|
|
120
|
+
stream: true,
|
|
121
|
+
// Caller-supplied empty string is still a value; only `undefined`
|
|
122
|
+
// (i.e. resolveSystemPrompt returned `null`) means "omit".
|
|
123
|
+
...built.system !== void 0 ? { system: built.system } : {},
|
|
124
|
+
messages: built.messages,
|
|
125
|
+
...config.body
|
|
126
|
+
});
|
|
127
|
+
const response = await fetch(url, {
|
|
128
|
+
method: "POST",
|
|
129
|
+
headers,
|
|
130
|
+
body,
|
|
131
|
+
signal
|
|
132
|
+
});
|
|
133
|
+
if (!response.ok) {
|
|
134
|
+
throw await readErrorBody(response, PROVIDER_NAME);
|
|
135
|
+
}
|
|
136
|
+
for await (const payload of parseSSE(response, signal)) {
|
|
137
|
+
let event;
|
|
138
|
+
try {
|
|
139
|
+
event = JSON.parse(payload);
|
|
140
|
+
} catch (e) {
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
if (event.type === "content_block_delta" && ((_d = event.delta) == null ? void 0 : _d.type) === "text_delta" && event.delta.text) {
|
|
144
|
+
yield event.delta.text;
|
|
145
|
+
}
|
|
146
|
+
if (event.type === "message_stop") return;
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
exports.createAnthropicProvider = createAnthropicProvider;
|
|
152
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sources":["../../../../src/llm-providers/shared.ts","../../../../src/llm-providers/anthropic/index.ts"],"sourcesContent":["import type { AIPromptContext } from '../feature/ai/types'\n\n/// Default system prompt used by both `createOpenAIProvider` and\n/// `createAnthropicProvider`. Constrains the model to emit raw markdown\n/// suitable for the streaming + diff plugins to apply directly.\nexport const DEFAULT_SYSTEM_PROMPT = `You are a writing assistant embedded in a markdown editor.\n\nRules:\n- Output markdown only. Never wrap your output in code fences (e.g. \\`\\`\\`markdown ... \\`\\`\\`).\n- Never include preambles, explanations, or sign-offs — output only the edited or generated content itself.\n- Preserve the original markdown structure (headings, lists, links, code blocks) unless the instruction explicitly asks to change it.\n- If a <selection> is provided, return only the replacement for that selection — do not repeat surrounding document context.\n- If no <selection> is provided, return content to insert at the cursor that flows with the surrounding document.`\n\n/// Default user-message body. Wraps document, selection, and\n/// instruction in XML-ish tags so the model can tell them apart.\nexport function buildDefaultUserMessage(context: AIPromptContext): string {\n const parts: string[] = [`<document>\\n${context.document}\\n</document>`]\n if (context.selection) {\n parts.push(`<selection>\\n${context.selection}\\n</selection>`)\n }\n parts.push(`<instruction>\\n${context.instruction}\\n</instruction>`)\n return parts.join('\\n\\n')\n}\n\n/// Common config shared by every built-in provider.\nexport interface BaseProviderConfig {\n /// API key used when calling the provider directly. Omit when routing\n /// through your own backend (set `baseURL` + `headers` instead).\n apiKey?: string\n\n /// Override the API base URL. Defaults to the provider's official\n /// endpoint. Point this at your own backend proxy in production\n /// deployments so the API key never reaches the browser.\n baseURL?: string\n\n /// Extra headers merged into every request. Use this to inject your\n /// app's session token when proxying through your backend.\n headers?: Record<string, string>\n\n /// Model identifier (e.g. `gpt-4o-mini`, `claude-sonnet-4-5`).\n model: string\n\n /// System prompt. Defaults to a markdown-output-only prompt that\n /// works well with the streaming + diff plugins. Pass a custom string\n /// to fully replace it; pass `null` to send no system prompt at all.\n systemPrompt?: string | null\n\n /// Required when calling the provider directly from a browser with an\n /// `apiKey`. Setting this to `true` is an explicit acknowledgement\n /// that your API key will be visible to anyone inspecting network\n /// traffic. Recommended only for desktop apps, personal tools, or\n /// BYOK setups where each user supplies their own key. Routing\n /// through a backend (via `baseURL` + `headers`) does not need this.\n dangerouslyAllowBrowser?: boolean\n}\n\n/// Detects any client-side execution context where the API key would\n/// be visible to user-controlled code: the main browser thread (DOM)\n/// and Web/Service/Shared/Worklet workers (`WorkerGlobalScope` is the\n/// global type in worker contexts). Node/SSR has neither.\n/// Computed on each call (rather than at module load) so tests can\n/// stub the relevant globals.\nfunction isBrowserLike(): boolean {\n const g = globalThis as Record<string, unknown>\n if (g.document !== undefined) return true\n if (g.WorkerGlobalScope !== undefined) return true\n return false\n}\n\n/// Throws when `apiKey` is set in a client-side context (main browser\n/// thread or Worker) without explicit `dangerouslyAllowBrowser` opt-in.\n/// Routing through a backend proxy (no `apiKey`, with `baseURL` +\n/// `headers`) bypasses this check because the key never reaches the\n/// client.\nexport function assertBrowserSafe(\n config: BaseProviderConfig,\n providerName: string\n): void {\n if (!config.apiKey) return\n if (!isBrowserLike()) return\n if (config.dangerouslyAllowBrowser) return\n throw new Error(\n `[${providerName}] Refusing to send your API key from a browser. ` +\n `Direct browser → provider calls expose the key to every visitor. ` +\n `Either route through your backend (set \\`baseURL\\` + \\`headers\\` ` +\n `and omit \\`apiKey\\`), or set \\`dangerouslyAllowBrowser: true\\` to ` +\n `acknowledge the risk (recommended only for desktop apps or BYOK setups).`\n )\n}\n\n/// Resolve the system prompt. `undefined` → default, `null` → omitted,\n/// string → used as-is.\nexport function resolveSystemPrompt(\n systemPrompt: string | null | undefined\n): string | null {\n if (systemPrompt === null) return null\n return systemPrompt ?? DEFAULT_SYSTEM_PROMPT\n}\n\n/// Strip trailing `/` characters from a URL. Uses a linear scan instead\n/// of a regex like `/\\/+$/` because the latter backtracks quadratically\n/// on caller-supplied input ending in many slashes followed by a\n/// non-slash (CodeQL js/polynomial-redos).\nexport function stripTrailingSlashes(url: string): string {\n let end = url.length\n while (end > 0 && url.charCodeAt(end - 1) === 47 /* '/' */) end--\n return end === url.length ? url : url.slice(0, end)\n}\n\n/// Parse an SSE stream from `response.body`. Yields the payload after\n/// `data: ` for each event; ignores `event:`, `id:`, `retry:`, and\n/// comment lines. Stops cleanly when the signal aborts or the stream\n/// ends. Both OpenAI and Anthropic streaming endpoints use this format.\nexport async function* parseSSE(\n response: Response,\n signal: AbortSignal\n): AsyncGenerator<string> {\n if (!response.body) return\n const reader = response.body.getReader()\n const decoder = new TextDecoder('utf-8')\n let buffer = ''\n try {\n while (true) {\n if (signal.aborted) return\n const { done, value } = await reader.read()\n if (signal.aborted) return\n if (done) break\n buffer += decoder.decode(value, { stream: true })\n let nl: number\n while ((nl = buffer.indexOf('\\n')) >= 0) {\n const raw = buffer.slice(0, nl)\n buffer = buffer.slice(nl + 1)\n const line = raw.endsWith('\\r') ? raw.slice(0, -1) : raw\n if (line.startsWith('data: ')) {\n yield line.slice(6)\n } else if (line.startsWith('data:')) {\n yield line.slice(5)\n }\n }\n }\n // Flush any trailing data line that wasn't newline-terminated.\n // Only strip a trailing `\\r` (handle CRLF without an LF) — never\n // `trim()` here, since the payload's leading/trailing whitespace\n // is significant for streamed token content.\n const tail = buffer.endsWith('\\r') ? buffer.slice(0, -1) : buffer\n if (tail.startsWith('data: ')) yield tail.slice(6)\n else if (tail.startsWith('data:')) yield tail.slice(5)\n } finally {\n reader.releaseLock()\n }\n}\n\n/// Throw a useful error when the API responds with a non-2xx status.\n/// Reads the body once for diagnostics.\nexport async function readErrorBody(\n response: Response,\n providerName: string\n): Promise<Error> {\n let body = ''\n try {\n body = await response.text()\n } catch {\n // ignore — we still want to throw a status-only error\n }\n return new Error(\n `[${providerName}] Request failed with status ${response.status}` +\n (body ? `: ${body.slice(0, 500)}` : '')\n )\n}\n","import type { AIPromptContext, AIProvider } from '../../feature/ai/types'\n\nimport {\n type BaseProviderConfig,\n assertBrowserSafe,\n buildDefaultUserMessage,\n parseSSE,\n readErrorBody,\n resolveSystemPrompt,\n stripTrailingSlashes,\n} from '../shared'\n\nconst PROVIDER_NAME = 'milkdown/providers/anthropic'\nconst DEFAULT_BASE_URL = 'https://api.anthropic.com'\nconst DEFAULT_ANTHROPIC_VERSION = '2023-06-01'\nconst DEFAULT_MAX_TOKENS = 4096\n\nexport interface AnthropicMessage {\n role: 'user' | 'assistant'\n content: string\n}\n\nexport interface AnthropicBuildMessagesResult {\n /// Anthropic puts the system prompt in a top-level `system` field,\n /// not in the messages array.\n system?: string\n messages: AnthropicMessage[]\n}\n\nexport interface AnthropicProviderConfig extends BaseProviderConfig {\n /// Anthropic's API requires `max_tokens`. Defaults to 4096.\n maxTokens?: number\n\n /// `anthropic-version` header. Defaults to `2023-06-01`.\n anthropicVersion?: string\n\n /// Customize the request payload. The default builder maps the\n /// resolved system prompt to `system` and produces a single user\n /// message containing the document, selection, and instruction.\n buildMessages?: (\n context: AIPromptContext,\n defaults: { systemPrompt: string | null; userMessage: string }\n ) => AnthropicBuildMessagesResult\n\n /// Extra fields merged into the request body (e.g. `temperature`,\n /// `top_p`, `metadata`, `tool_choice`).\n body?: Record<string, unknown>\n}\n\ninterface AnthropicStreamEvent {\n type: string\n delta?: {\n type?: string\n text?: string\n }\n}\n\n/// Build an `AIProvider` backed by Anthropic's `/v1/messages` streaming\n/// endpoint. The returned function plugs directly into\n/// `Crepe.Feature.AI`'s `provider` field.\nexport function createAnthropicProvider(\n config: AnthropicProviderConfig\n): AIProvider {\n assertBrowserSafe(config, PROVIDER_NAME)\n\n return async function* anthropicProvider(context, signal) {\n const systemPrompt = resolveSystemPrompt(config.systemPrompt)\n const userMessage = buildDefaultUserMessage(context)\n\n const built = config.buildMessages\n ? config.buildMessages(context, { systemPrompt, userMessage })\n : {\n system: systemPrompt ?? undefined,\n messages: [\n { role: 'user' as const, content: userMessage },\n ] satisfies AnthropicMessage[],\n }\n\n const baseURL = stripTrailingSlashes(config.baseURL ?? DEFAULT_BASE_URL)\n const url = `${baseURL}/v1/messages`\n\n const headers: Record<string, string> = {\n 'Content-Type': 'application/json',\n 'anthropic-version': config.anthropicVersion ?? DEFAULT_ANTHROPIC_VERSION,\n ...(config.apiKey ? { 'x-api-key': config.apiKey } : {}),\n // Required for direct browser → api.anthropic.com calls; harmless\n // when proxying through a backend.\n ...(config.dangerouslyAllowBrowser\n ? { 'anthropic-dangerous-direct-browser-access': 'true' }\n : {}),\n ...config.headers,\n }\n\n const body = JSON.stringify({\n model: config.model,\n max_tokens: config.maxTokens ?? DEFAULT_MAX_TOKENS,\n stream: true,\n // Caller-supplied empty string is still a value; only `undefined`\n // (i.e. resolveSystemPrompt returned `null`) means \"omit\".\n ...(built.system !== undefined ? { system: built.system } : {}),\n messages: built.messages,\n ...config.body,\n })\n\n const response = await fetch(url, {\n method: 'POST',\n headers,\n body,\n signal,\n })\n\n if (!response.ok) {\n throw await readErrorBody(response, PROVIDER_NAME)\n }\n\n for await (const payload of parseSSE(response, signal)) {\n let event: AnthropicStreamEvent\n try {\n event = JSON.parse(payload) as AnthropicStreamEvent\n } catch {\n continue\n }\n if (\n event.type === 'content_block_delta' &&\n event.delta?.type === 'text_delta' &&\n event.delta.text\n ) {\n yield event.delta.text\n }\n if (event.type === 'message_stop') return\n }\n }\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const DEFAULT_SYSTEM_PROMPT = `You are a writing assistant embedded in a markdown editor.
|
|
4
|
+
|
|
5
|
+
Rules:
|
|
6
|
+
- Output markdown only. Never wrap your output in code fences (e.g. \`\`\`markdown ... \`\`\`).
|
|
7
|
+
- Never include preambles, explanations, or sign-offs \u2014 output only the edited or generated content itself.
|
|
8
|
+
- Preserve the original markdown structure (headings, lists, links, code blocks) unless the instruction explicitly asks to change it.
|
|
9
|
+
- If a <selection> is provided, return only the replacement for that selection \u2014 do not repeat surrounding document context.
|
|
10
|
+
- If no <selection> is provided, return content to insert at the cursor that flows with the surrounding document.`;
|
|
11
|
+
function buildDefaultUserMessage(context) {
|
|
12
|
+
const parts = [`<document>
|
|
13
|
+
${context.document}
|
|
14
|
+
</document>`];
|
|
15
|
+
if (context.selection) {
|
|
16
|
+
parts.push(`<selection>
|
|
17
|
+
${context.selection}
|
|
18
|
+
</selection>`);
|
|
19
|
+
}
|
|
20
|
+
parts.push(`<instruction>
|
|
21
|
+
${context.instruction}
|
|
22
|
+
</instruction>`);
|
|
23
|
+
return parts.join("\n\n");
|
|
24
|
+
}
|
|
25
|
+
function isBrowserLike() {
|
|
26
|
+
const g = globalThis;
|
|
27
|
+
if (g.document !== void 0) return true;
|
|
28
|
+
if (g.WorkerGlobalScope !== void 0) return true;
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
function assertBrowserSafe(config, providerName) {
|
|
32
|
+
if (!config.apiKey) return;
|
|
33
|
+
if (!isBrowserLike()) return;
|
|
34
|
+
if (config.dangerouslyAllowBrowser) return;
|
|
35
|
+
throw new Error(
|
|
36
|
+
`[${providerName}] Refusing to send your API key from a browser. Direct browser \u2192 provider calls expose the key to every visitor. Either route through your backend (set \`baseURL\` + \`headers\` and omit \`apiKey\`), or set \`dangerouslyAllowBrowser: true\` to acknowledge the risk (recommended only for desktop apps or BYOK setups).`
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
function resolveSystemPrompt(systemPrompt) {
|
|
40
|
+
if (systemPrompt === null) return null;
|
|
41
|
+
return systemPrompt != null ? systemPrompt : DEFAULT_SYSTEM_PROMPT;
|
|
42
|
+
}
|
|
43
|
+
function stripTrailingSlashes(url) {
|
|
44
|
+
let end = url.length;
|
|
45
|
+
while (end > 0 && url.charCodeAt(end - 1) === 47) end--;
|
|
46
|
+
return end === url.length ? url : url.slice(0, end);
|
|
47
|
+
}
|
|
48
|
+
async function* parseSSE(response, signal) {
|
|
49
|
+
if (!response.body) return;
|
|
50
|
+
const reader = response.body.getReader();
|
|
51
|
+
const decoder = new TextDecoder("utf-8");
|
|
52
|
+
let buffer = "";
|
|
53
|
+
try {
|
|
54
|
+
while (true) {
|
|
55
|
+
if (signal.aborted) return;
|
|
56
|
+
const { done, value } = await reader.read();
|
|
57
|
+
if (signal.aborted) return;
|
|
58
|
+
if (done) break;
|
|
59
|
+
buffer += decoder.decode(value, { stream: true });
|
|
60
|
+
let nl;
|
|
61
|
+
while ((nl = buffer.indexOf("\n")) >= 0) {
|
|
62
|
+
const raw = buffer.slice(0, nl);
|
|
63
|
+
buffer = buffer.slice(nl + 1);
|
|
64
|
+
const line = raw.endsWith("\r") ? raw.slice(0, -1) : raw;
|
|
65
|
+
if (line.startsWith("data: ")) {
|
|
66
|
+
yield line.slice(6);
|
|
67
|
+
} else if (line.startsWith("data:")) {
|
|
68
|
+
yield line.slice(5);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
const tail = buffer.endsWith("\r") ? buffer.slice(0, -1) : buffer;
|
|
73
|
+
if (tail.startsWith("data: ")) yield tail.slice(6);
|
|
74
|
+
else if (tail.startsWith("data:")) yield tail.slice(5);
|
|
75
|
+
} finally {
|
|
76
|
+
reader.releaseLock();
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
async function readErrorBody(response, providerName) {
|
|
80
|
+
let body = "";
|
|
81
|
+
try {
|
|
82
|
+
body = await response.text();
|
|
83
|
+
} catch (e) {
|
|
84
|
+
}
|
|
85
|
+
return new Error(
|
|
86
|
+
`[${providerName}] Request failed with status ${response.status}` + (body ? `: ${body.slice(0, 500)}` : "")
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const PROVIDER_NAME = "milkdown/providers/openai";
|
|
91
|
+
const DEFAULT_BASE_URL = "https://api.openai.com";
|
|
92
|
+
function createOpenAIProvider(config) {
|
|
93
|
+
assertBrowserSafe(config, PROVIDER_NAME);
|
|
94
|
+
return async function* openAIProvider(context, signal) {
|
|
95
|
+
var _a, _b, _c, _d;
|
|
96
|
+
const systemPrompt = resolveSystemPrompt(config.systemPrompt);
|
|
97
|
+
const userMessage = buildDefaultUserMessage(context);
|
|
98
|
+
const messages = config.buildMessages ? config.buildMessages(context, { systemPrompt, userMessage }) : defaultMessages(systemPrompt, userMessage);
|
|
99
|
+
const baseURL = stripTrailingSlashes((_a = config.baseURL) != null ? _a : DEFAULT_BASE_URL);
|
|
100
|
+
const url = `${baseURL}/v1/chat/completions`;
|
|
101
|
+
const headers = {
|
|
102
|
+
"Content-Type": "application/json",
|
|
103
|
+
...config.apiKey ? { Authorization: `Bearer ${config.apiKey}` } : {},
|
|
104
|
+
...config.headers
|
|
105
|
+
};
|
|
106
|
+
const body = JSON.stringify({
|
|
107
|
+
model: config.model,
|
|
108
|
+
stream: true,
|
|
109
|
+
messages,
|
|
110
|
+
...config.body
|
|
111
|
+
});
|
|
112
|
+
const response = await fetch(url, {
|
|
113
|
+
method: "POST",
|
|
114
|
+
headers,
|
|
115
|
+
body,
|
|
116
|
+
signal
|
|
117
|
+
});
|
|
118
|
+
if (!response.ok) {
|
|
119
|
+
throw await readErrorBody(response, PROVIDER_NAME);
|
|
120
|
+
}
|
|
121
|
+
for await (const payload of parseSSE(response, signal)) {
|
|
122
|
+
if (payload === "[DONE]") return;
|
|
123
|
+
let event;
|
|
124
|
+
try {
|
|
125
|
+
event = JSON.parse(payload);
|
|
126
|
+
} catch (e) {
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
const delta = (_d = (_c = (_b = event.choices) == null ? void 0 : _b[0]) == null ? void 0 : _c.delta) == null ? void 0 : _d.content;
|
|
130
|
+
if (delta) yield delta;
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
function defaultMessages(systemPrompt, userMessage) {
|
|
135
|
+
const messages = [];
|
|
136
|
+
if (systemPrompt !== null)
|
|
137
|
+
messages.push({ role: "system", content: systemPrompt });
|
|
138
|
+
messages.push({ role: "user", content: userMessage });
|
|
139
|
+
return messages;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
exports.createOpenAIProvider = createOpenAIProvider;
|
|
143
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sources":["../../../../src/llm-providers/shared.ts","../../../../src/llm-providers/openai/index.ts"],"sourcesContent":["import type { AIPromptContext } from '../feature/ai/types'\n\n/// Default system prompt used by both `createOpenAIProvider` and\n/// `createAnthropicProvider`. Constrains the model to emit raw markdown\n/// suitable for the streaming + diff plugins to apply directly.\nexport const DEFAULT_SYSTEM_PROMPT = `You are a writing assistant embedded in a markdown editor.\n\nRules:\n- Output markdown only. Never wrap your output in code fences (e.g. \\`\\`\\`markdown ... \\`\\`\\`).\n- Never include preambles, explanations, or sign-offs — output only the edited or generated content itself.\n- Preserve the original markdown structure (headings, lists, links, code blocks) unless the instruction explicitly asks to change it.\n- If a <selection> is provided, return only the replacement for that selection — do not repeat surrounding document context.\n- If no <selection> is provided, return content to insert at the cursor that flows with the surrounding document.`\n\n/// Default user-message body. Wraps document, selection, and\n/// instruction in XML-ish tags so the model can tell them apart.\nexport function buildDefaultUserMessage(context: AIPromptContext): string {\n const parts: string[] = [`<document>\\n${context.document}\\n</document>`]\n if (context.selection) {\n parts.push(`<selection>\\n${context.selection}\\n</selection>`)\n }\n parts.push(`<instruction>\\n${context.instruction}\\n</instruction>`)\n return parts.join('\\n\\n')\n}\n\n/// Common config shared by every built-in provider.\nexport interface BaseProviderConfig {\n /// API key used when calling the provider directly. Omit when routing\n /// through your own backend (set `baseURL` + `headers` instead).\n apiKey?: string\n\n /// Override the API base URL. Defaults to the provider's official\n /// endpoint. Point this at your own backend proxy in production\n /// deployments so the API key never reaches the browser.\n baseURL?: string\n\n /// Extra headers merged into every request. Use this to inject your\n /// app's session token when proxying through your backend.\n headers?: Record<string, string>\n\n /// Model identifier (e.g. `gpt-4o-mini`, `claude-sonnet-4-5`).\n model: string\n\n /// System prompt. Defaults to a markdown-output-only prompt that\n /// works well with the streaming + diff plugins. Pass a custom string\n /// to fully replace it; pass `null` to send no system prompt at all.\n systemPrompt?: string | null\n\n /// Required when calling the provider directly from a browser with an\n /// `apiKey`. Setting this to `true` is an explicit acknowledgement\n /// that your API key will be visible to anyone inspecting network\n /// traffic. Recommended only for desktop apps, personal tools, or\n /// BYOK setups where each user supplies their own key. Routing\n /// through a backend (via `baseURL` + `headers`) does not need this.\n dangerouslyAllowBrowser?: boolean\n}\n\n/// Detects any client-side execution context where the API key would\n/// be visible to user-controlled code: the main browser thread (DOM)\n/// and Web/Service/Shared/Worklet workers (`WorkerGlobalScope` is the\n/// global type in worker contexts). Node/SSR has neither.\n/// Computed on each call (rather than at module load) so tests can\n/// stub the relevant globals.\nfunction isBrowserLike(): boolean {\n const g = globalThis as Record<string, unknown>\n if (g.document !== undefined) return true\n if (g.WorkerGlobalScope !== undefined) return true\n return false\n}\n\n/// Throws when `apiKey` is set in a client-side context (main browser\n/// thread or Worker) without explicit `dangerouslyAllowBrowser` opt-in.\n/// Routing through a backend proxy (no `apiKey`, with `baseURL` +\n/// `headers`) bypasses this check because the key never reaches the\n/// client.\nexport function assertBrowserSafe(\n config: BaseProviderConfig,\n providerName: string\n): void {\n if (!config.apiKey) return\n if (!isBrowserLike()) return\n if (config.dangerouslyAllowBrowser) return\n throw new Error(\n `[${providerName}] Refusing to send your API key from a browser. ` +\n `Direct browser → provider calls expose the key to every visitor. ` +\n `Either route through your backend (set \\`baseURL\\` + \\`headers\\` ` +\n `and omit \\`apiKey\\`), or set \\`dangerouslyAllowBrowser: true\\` to ` +\n `acknowledge the risk (recommended only for desktop apps or BYOK setups).`\n )\n}\n\n/// Resolve the system prompt. `undefined` → default, `null` → omitted,\n/// string → used as-is.\nexport function resolveSystemPrompt(\n systemPrompt: string | null | undefined\n): string | null {\n if (systemPrompt === null) return null\n return systemPrompt ?? DEFAULT_SYSTEM_PROMPT\n}\n\n/// Strip trailing `/` characters from a URL. Uses a linear scan instead\n/// of a regex like `/\\/+$/` because the latter backtracks quadratically\n/// on caller-supplied input ending in many slashes followed by a\n/// non-slash (CodeQL js/polynomial-redos).\nexport function stripTrailingSlashes(url: string): string {\n let end = url.length\n while (end > 0 && url.charCodeAt(end - 1) === 47 /* '/' */) end--\n return end === url.length ? url : url.slice(0, end)\n}\n\n/// Parse an SSE stream from `response.body`. Yields the payload after\n/// `data: ` for each event; ignores `event:`, `id:`, `retry:`, and\n/// comment lines. Stops cleanly when the signal aborts or the stream\n/// ends. Both OpenAI and Anthropic streaming endpoints use this format.\nexport async function* parseSSE(\n response: Response,\n signal: AbortSignal\n): AsyncGenerator<string> {\n if (!response.body) return\n const reader = response.body.getReader()\n const decoder = new TextDecoder('utf-8')\n let buffer = ''\n try {\n while (true) {\n if (signal.aborted) return\n const { done, value } = await reader.read()\n if (signal.aborted) return\n if (done) break\n buffer += decoder.decode(value, { stream: true })\n let nl: number\n while ((nl = buffer.indexOf('\\n')) >= 0) {\n const raw = buffer.slice(0, nl)\n buffer = buffer.slice(nl + 1)\n const line = raw.endsWith('\\r') ? raw.slice(0, -1) : raw\n if (line.startsWith('data: ')) {\n yield line.slice(6)\n } else if (line.startsWith('data:')) {\n yield line.slice(5)\n }\n }\n }\n // Flush any trailing data line that wasn't newline-terminated.\n // Only strip a trailing `\\r` (handle CRLF without an LF) — never\n // `trim()` here, since the payload's leading/trailing whitespace\n // is significant for streamed token content.\n const tail = buffer.endsWith('\\r') ? buffer.slice(0, -1) : buffer\n if (tail.startsWith('data: ')) yield tail.slice(6)\n else if (tail.startsWith('data:')) yield tail.slice(5)\n } finally {\n reader.releaseLock()\n }\n}\n\n/// Throw a useful error when the API responds with a non-2xx status.\n/// Reads the body once for diagnostics.\nexport async function readErrorBody(\n response: Response,\n providerName: string\n): Promise<Error> {\n let body = ''\n try {\n body = await response.text()\n } catch {\n // ignore — we still want to throw a status-only error\n }\n return new Error(\n `[${providerName}] Request failed with status ${response.status}` +\n (body ? `: ${body.slice(0, 500)}` : '')\n )\n}\n","import type { AIPromptContext, AIProvider } from '../../feature/ai/types'\n\nimport {\n type BaseProviderConfig,\n assertBrowserSafe,\n buildDefaultUserMessage,\n parseSSE,\n readErrorBody,\n resolveSystemPrompt,\n stripTrailingSlashes,\n} from '../shared'\n\nconst PROVIDER_NAME = 'milkdown/providers/openai'\nconst DEFAULT_BASE_URL = 'https://api.openai.com'\n\nexport interface OpenAIChatMessage {\n role: 'system' | 'user' | 'assistant'\n content: string\n}\n\nexport interface OpenAIProviderConfig extends BaseProviderConfig {\n /// Customize the messages sent to `/v1/chat/completions`. The default\n /// builder produces a system message (when not disabled) followed by\n /// a structured user message containing the document, selection, and\n /// instruction. The defaults are passed in so callers can wrap them\n /// rather than re-deriving from scratch.\n buildMessages?: (\n context: AIPromptContext,\n defaults: { systemPrompt: string | null; userMessage: string }\n ) => OpenAIChatMessage[]\n\n /// Extra fields merged into the request body (e.g. `temperature`,\n /// `top_p`, `presence_penalty`, `response_format`).\n body?: Record<string, unknown>\n}\n\ninterface OpenAIStreamChunk {\n choices?: Array<{\n delta?: { content?: string }\n finish_reason?: string | null\n }>\n}\n\n/// Build an `AIProvider` backed by OpenAI's `/v1/chat/completions`\n/// streaming endpoint. The returned function plugs directly into\n/// `Crepe.Feature.AI`'s `provider` field.\nexport function createOpenAIProvider(config: OpenAIProviderConfig): AIProvider {\n assertBrowserSafe(config, PROVIDER_NAME)\n\n return async function* openAIProvider(context, signal) {\n const systemPrompt = resolveSystemPrompt(config.systemPrompt)\n const userMessage = buildDefaultUserMessage(context)\n\n const messages = config.buildMessages\n ? config.buildMessages(context, { systemPrompt, userMessage })\n : defaultMessages(systemPrompt, userMessage)\n\n const baseURL = stripTrailingSlashes(config.baseURL ?? DEFAULT_BASE_URL)\n const url = `${baseURL}/v1/chat/completions`\n\n const headers: Record<string, string> = {\n 'Content-Type': 'application/json',\n ...(config.apiKey ? { Authorization: `Bearer ${config.apiKey}` } : {}),\n ...config.headers,\n }\n\n const body = JSON.stringify({\n model: config.model,\n stream: true,\n messages,\n ...config.body,\n })\n\n const response = await fetch(url, {\n method: 'POST',\n headers,\n body,\n signal,\n })\n\n if (!response.ok) {\n throw await readErrorBody(response, PROVIDER_NAME)\n }\n\n for await (const payload of parseSSE(response, signal)) {\n if (payload === '[DONE]') return\n let event: OpenAIStreamChunk\n try {\n event = JSON.parse(payload) as OpenAIStreamChunk\n } catch {\n continue\n }\n const delta = event.choices?.[0]?.delta?.content\n if (delta) yield delta\n }\n }\n}\n\nfunction defaultMessages(\n systemPrompt: string | null,\n userMessage: string\n): OpenAIChatMessage[] {\n const messages: OpenAIChatMessage[] = []\n // `null` means \"omit\"; an empty string is still a caller-provided\n // value and should be sent as-is.\n if (systemPrompt !== null)\n messages.push({ role: 'system', content: systemPrompt })\n messages.push({ role: 'user', content: userMessage })\n return messages\n}\n"],"names":[],"mappings":"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"}
|
package/lib/esm/builder.js
CHANGED
package/lib/esm/builder.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"builder.js","sources":["../../src/feature/index.ts","../../src/core/slice.ts","../../src/core/builder.ts"],"sourcesContent":["import type { BlockEditFeatureConfig } from './block-edit'\nimport type { CodeMirrorFeatureConfig } from './code-mirror'\nimport type { CursorFeatureConfig } from './cursor'\nimport type { ImageBlockFeatureConfig } from './image-block'\nimport type { LatexFeatureConfig } from './latex'\nimport type { LinkTooltipFeatureConfig } from './link-tooltip'\nimport type { ListItemFeatureConfig } from './list-item'\nimport type { PlaceholderFeatureConfig } from './placeholder'\nimport type { TableFeatureConfig } from './table'\nimport type { ToolbarFeatureConfig } from './toolbar'\nimport type { TopBarFeatureConfig } from './top-bar'\n\n/// The crepe editor feature flags.\n/// Every feature is enabled by default.\n/// Every feature is a string literal type.\nexport enum CrepeFeature {\n /// Syntax highlighting and editing for code blocks with language support, theme customization, and preview capabilities.\n CodeMirror = 'code-mirror',\n\n /// Support for bullet lists, ordered lists, and todo lists with customizable icons and formatting.\n ListItem = 'list-item',\n\n /// Enhanced link editing and preview with customizable tooltips, edit/remove actions, and copy functionality.\n LinkTooltip = 'link-tooltip',\n\n /// Enhanced cursor experience with drop cursor and gap cursor for better content placement.\n Cursor = 'cursor',\n\n /// Image upload and management with resizing, captions, and support for both inline and block images.\n ImageBlock = 'image-block',\n\n /// Drag-and-drop block management and slash commands for quick content insertion and organization.\n BlockEdit = 'block-edit',\n\n /// Formatting toolbar for selected text with customizable icons and actions.\n Toolbar = 'toolbar',\n\n /// Document or block level placeholders to guide users when content is empty.\n Placeholder = 'placeholder',\n\n /// Full-featured table editing with row/column management, alignment options, and drag-and-drop functionality.\n Table = 'table',\n\n /// Mathematical formula support with both inline and block math rendering using KaTeX.\n Latex = 'latex',\n\n /// Fixed top toolbar with heading selector, formatting buttons, insert actions, and block commands.\n TopBar = 'top-bar',\n}\n\nexport interface CrepeFeatureConfig {\n [CrepeFeature.Cursor]?: CursorFeatureConfig\n [CrepeFeature.ListItem]?: ListItemFeatureConfig\n [CrepeFeature.LinkTooltip]?: LinkTooltipFeatureConfig\n [CrepeFeature.ImageBlock]?: ImageBlockFeatureConfig\n [CrepeFeature.BlockEdit]?: BlockEditFeatureConfig\n [CrepeFeature.Placeholder]?: PlaceholderFeatureConfig\n [CrepeFeature.Toolbar]?: ToolbarFeatureConfig\n [CrepeFeature.CodeMirror]?: CodeMirrorFeatureConfig\n [CrepeFeature.Table]?: TableFeatureConfig\n [CrepeFeature.Latex]?: LatexFeatureConfig\n [CrepeFeature.TopBar]?: TopBarFeatureConfig\n}\n\nexport const defaultFeatures: Record<CrepeFeature, boolean> = {\n [CrepeFeature.Cursor]: true,\n [CrepeFeature.ListItem]: true,\n [CrepeFeature.LinkTooltip]: true,\n [CrepeFeature.ImageBlock]: true,\n [CrepeFeature.BlockEdit]: true,\n [CrepeFeature.Placeholder]: true,\n [CrepeFeature.Toolbar]: true,\n [CrepeFeature.CodeMirror]: true,\n [CrepeFeature.Table]: true,\n [CrepeFeature.Latex]: true,\n [CrepeFeature.TopBar]: false,\n}\n","import { createSlice, type Ctx } from '@milkdown/kit/ctx'\n\nimport type { CrepeFeature } from '../feature'\nimport type { CrepeBuilder } from './builder'\n\n/// @internal\n/// The feature flags context.\n/// ⚠️ Most of the time, you should use `useCrepeFeatures` to get the features.\nexport const FeaturesCtx = createSlice([] as CrepeFeature[], 'FeaturesCtx')\n\n/// @internal\n/// The crepe editor context.\n/// ⚠️ Most of the time, you should use `useCrepe` to get the crepe editor instance.\nexport const CrepeCtx = createSlice({} as CrepeBuilder, 'CrepeCtx')\n\n/// The crepe editor context.\n/// You can use this context to access the crepe editor instance within Milkdown plugins.\n/// ```ts\n/// import { crepeCtx } from '@milkdown/crepe'\n/// const plugin = (ctx: Ctx) => {\n/// return () => {\n/// const crepe = useCrepe(ctx)\n/// crepe.setReadonly(true)\n/// }\n/// }\n/// ```\nexport function useCrepe(ctx: Ctx) {\n // We should use string slice here to avoid the slice to be bundled in multiple entries\n return ctx.get<CrepeBuilder, 'CrepeCtx'>('CrepeCtx')\n}\n\n/// Check the enabled FeatureFlags\n/// ```ts\n/// import { useCrepeFeatures } from '@milkdown/crepe'\n/// const plugin = (ctx: Ctx) => {\n/// const features = useCrepeFeatures(ctx)\n/// if (features.get().includes(CrepeFeature.CodeMirror)) {\n/// // Do something with CodeMirror\n/// }\n/// }\nexport function useCrepeFeatures(ctx: Ctx) {\n // We should use string slice here to avoid the slice to be bundled in multiple entries\n return ctx.use<CrepeFeature[], 'FeaturesCtx'>('FeaturesCtx')\n}\n\n/// @internal\nexport function crepeFeatureConfig(feature: CrepeFeature) {\n return (ctx: Ctx) => {\n useCrepeFeatures(ctx).update((features) => {\n if (features.includes(feature)) {\n return features\n }\n return [...features, feature]\n })\n }\n}\n","import { imageBlockConfig } from '@milkdown/kit/component/image-block'\nimport {\n type DefaultValue,\n defaultValueCtx,\n Editor,\n EditorStatus,\n editorViewCtx,\n editorViewOptionsCtx,\n rootCtx,\n} from '@milkdown/kit/core'\nimport { clipboard } from '@milkdown/kit/plugin/clipboard'\nimport { history } from '@milkdown/kit/plugin/history'\nimport { indent, indentConfig } from '@milkdown/kit/plugin/indent'\nimport {\n listener,\n listenerCtx,\n type ListenerManager,\n} from '@milkdown/kit/plugin/listener'\nimport { trailing } from '@milkdown/kit/plugin/trailing'\nimport { upload, uploadConfig } from '@milkdown/kit/plugin/upload'\nimport { commonmark } from '@milkdown/kit/preset/commonmark'\nimport { gfm } from '@milkdown/kit/preset/gfm'\nimport { getMarkdown } from '@milkdown/kit/utils'\n\nimport type { CrepeFeatureConfig } from '../feature'\nimport type { DefineFeature } from '../feature/shared'\n\nimport { CrepeFeature } from '../feature'\nimport { CrepeCtx, FeaturesCtx, useCrepeFeatures } from './slice'\n\n/// The crepe builder configuration.\nexport interface CrepeBuilderConfig {\n /// The root element for the editor.\n /// Supports both DOM nodes and CSS selectors,\n /// If not provided, the editor will be appended to the body.\n root?: Node | string | null\n\n /// The default value for the editor.\n defaultValue?: DefaultValue\n}\n\n/// The crepe builder class.\n/// This class allows users to manually add features to the editor.\nexport class CrepeBuilder {\n /// @internal\n readonly #editor: Editor\n\n /// @internal\n readonly #rootElement: Node\n\n /// @internal\n #editable = true\n\n /// The constructor of the crepe builder.\n /// You can pass configs to the builder to configure the editor.\n constructor({ root, defaultValue = '' }: CrepeBuilderConfig = {}) {\n this.#rootElement =\n (typeof root === 'string' ? document.querySelector(root) : root) ??\n document.body\n this.#editor = Editor.make()\n .config((ctx) => {\n ctx.inject(CrepeCtx, this)\n ctx.inject(FeaturesCtx, [])\n })\n .config((ctx) => {\n ctx.set(rootCtx, this.#rootElement)\n ctx.set(defaultValueCtx, defaultValue)\n ctx.set(editorViewOptionsCtx, {\n editable: () => this.#editable,\n })\n ctx.update(indentConfig.key, (value) => ({\n ...value,\n size: 4,\n }))\n ctx.update(uploadConfig.key, (prev) => ({\n ...prev,\n uploader: async (files, schema, ctx) => {\n const features = useCrepeFeatures(ctx).get()\n const hasImageBlock = features.includes(CrepeFeature.ImageBlock)\n const nodeType = hasImageBlock\n ? schema.nodes['image-block']\n : schema.nodes['image']\n\n if (!nodeType) return []\n\n const onUpload = hasImageBlock\n ? ctx.get(imageBlockConfig.key).onUpload\n : undefined\n\n const images: File[] = []\n for (let i = 0; i < files.length; i++) {\n const file = files.item(i)\n if (file && file.type.includes('image')) images.push(file)\n }\n\n const nodes = await Promise.all(\n images.map(async (file) => {\n const src = onUpload\n ? await onUpload(file)\n : URL.createObjectURL(file)\n return nodeType.createAndFill({ src })!\n })\n )\n\n return nodes\n },\n }))\n })\n .use(commonmark)\n .use(listener)\n .use(history)\n .use(indent)\n .use(trailing)\n .use(clipboard)\n .use(upload)\n .use(gfm)\n }\n\n /// Add a feature to the editor.\n addFeature: {\n <T extends CrepeFeature>(\n feature: DefineFeature<CrepeFeatureConfig[T]>,\n config?: CrepeFeatureConfig[T]\n ): CrepeBuilder\n <C>(feature: DefineFeature<C>, config?: C): CrepeBuilder\n } = (feature: DefineFeature, config?: never) => {\n feature(this.#editor, config)\n return this\n }\n\n /// Create the editor.\n create = () => {\n return this.#editor.create()\n }\n\n /// Destroy the editor.\n destroy = () => {\n return this.#editor.destroy()\n }\n\n /// Get the milkdown editor instance.\n get editor(): Editor {\n return this.#editor\n }\n\n /// Get the readonly state of the editor.\n get readonly() {\n return !this.#editable\n }\n\n /// Set the readonly mode of the editor.\n setReadonly = (value: boolean) => {\n this.#editable = !value\n this.#editor.action((ctx) => {\n if (this.#editor.status === EditorStatus.Created) {\n const view = ctx.get(editorViewCtx)\n view.setProps({\n editable: () => !value,\n })\n }\n })\n return this\n }\n\n /// Get the markdown content of the editor.\n getMarkdown = () => {\n return this.#editor.action(getMarkdown())\n }\n\n /// Register event listeners.\n on = (fn: (api: ListenerManager) => void) => {\n if (this.#editor.status !== EditorStatus.Created) {\n this.#editor.config((ctx) => {\n const listener = ctx.get(listenerCtx)\n fn(listener)\n })\n return this\n }\n this.#editor.action((ctx) => {\n const listener = ctx.get(listenerCtx)\n fn(listener)\n })\n return this\n }\n}\n"],"names":["CrepeFeature","listener","ctx"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"builder.js","sources":["../../src/feature/index.ts","../../src/core/slice.ts","../../src/core/builder.ts"],"sourcesContent":["import type { AIFeatureConfig } from './ai'\nimport type { BlockEditFeatureConfig } from './block-edit'\nimport type { CodeMirrorFeatureConfig } from './code-mirror'\nimport type { CursorFeatureConfig } from './cursor'\nimport type { ImageBlockFeatureConfig } from './image-block'\nimport type { LatexFeatureConfig } from './latex'\nimport type { LinkTooltipFeatureConfig } from './link-tooltip'\nimport type { ListItemFeatureConfig } from './list-item'\nimport type { PlaceholderFeatureConfig } from './placeholder'\nimport type { TableFeatureConfig } from './table'\nimport type { ToolbarFeatureConfig } from './toolbar'\nimport type { TopBarFeatureConfig } from './top-bar'\n\n/// The crepe editor feature flags.\n/// Most features are enabled by default; `TopBar` and `AI` are opt-in.\n/// See `defaultFeatures` for the per-flag default.\nexport enum CrepeFeature {\n /// Syntax highlighting and editing for code blocks with language support, theme customization, and preview capabilities.\n CodeMirror = 'code-mirror',\n\n /// Support for bullet lists, ordered lists, and todo lists with customizable icons and formatting.\n ListItem = 'list-item',\n\n /// Enhanced link editing and preview with customizable tooltips, edit/remove actions, and copy functionality.\n LinkTooltip = 'link-tooltip',\n\n /// Enhanced cursor experience with drop cursor and gap cursor for better content placement.\n Cursor = 'cursor',\n\n /// Image upload and management with resizing, captions, and support for both inline and block images.\n ImageBlock = 'image-block',\n\n /// Drag-and-drop block management and slash commands for quick content insertion and organization.\n BlockEdit = 'block-edit',\n\n /// Formatting toolbar for selected text with customizable icons and actions.\n Toolbar = 'toolbar',\n\n /// Document or block level placeholders to guide users when content is empty.\n Placeholder = 'placeholder',\n\n /// Full-featured table editing with row/column management, alignment options, and drag-and-drop functionality.\n Table = 'table',\n\n /// Mathematical formula support with both inline and block math rendering using KaTeX.\n Latex = 'latex',\n\n /// Fixed top toolbar with heading selector, formatting buttons, insert actions, and block commands.\n TopBar = 'top-bar',\n\n /// AI-assisted editing: streaming input, diff review, and provider integration.\n AI = 'ai',\n}\n\nexport interface CrepeFeatureConfig {\n [CrepeFeature.Cursor]?: CursorFeatureConfig\n [CrepeFeature.ListItem]?: ListItemFeatureConfig\n [CrepeFeature.LinkTooltip]?: LinkTooltipFeatureConfig\n [CrepeFeature.ImageBlock]?: ImageBlockFeatureConfig\n [CrepeFeature.BlockEdit]?: BlockEditFeatureConfig\n [CrepeFeature.Placeholder]?: PlaceholderFeatureConfig\n [CrepeFeature.Toolbar]?: ToolbarFeatureConfig\n [CrepeFeature.CodeMirror]?: CodeMirrorFeatureConfig\n [CrepeFeature.Table]?: TableFeatureConfig\n [CrepeFeature.Latex]?: LatexFeatureConfig\n [CrepeFeature.TopBar]?: TopBarFeatureConfig\n [CrepeFeature.AI]?: AIFeatureConfig\n}\n\nexport const defaultFeatures: Record<CrepeFeature, boolean> = {\n [CrepeFeature.Cursor]: true,\n [CrepeFeature.ListItem]: true,\n [CrepeFeature.LinkTooltip]: true,\n [CrepeFeature.ImageBlock]: true,\n [CrepeFeature.BlockEdit]: true,\n [CrepeFeature.Placeholder]: true,\n [CrepeFeature.Toolbar]: true,\n [CrepeFeature.CodeMirror]: true,\n [CrepeFeature.Table]: true,\n [CrepeFeature.Latex]: true,\n [CrepeFeature.TopBar]: false,\n [CrepeFeature.AI]: false,\n}\n","import { createSlice, type Ctx } from '@milkdown/kit/ctx'\n\nimport type { CrepeFeature } from '../feature'\nimport type { CrepeBuilder } from './builder'\n\n/// @internal\n/// The feature flags context.\n/// ⚠️ Most of the time, you should use `useCrepeFeatures` to get the features.\nexport const FeaturesCtx = createSlice([] as CrepeFeature[], 'FeaturesCtx')\n\n/// @internal\n/// The crepe editor context.\n/// ⚠️ Most of the time, you should use `useCrepe` to get the crepe editor instance.\nexport const CrepeCtx = createSlice({} as CrepeBuilder, 'CrepeCtx')\n\n/// The crepe editor context.\n/// You can use this context to access the crepe editor instance within Milkdown plugins.\n/// ```ts\n/// import { crepeCtx } from '@milkdown/crepe'\n/// const plugin = (ctx: Ctx) => {\n/// return () => {\n/// const crepe = useCrepe(ctx)\n/// crepe.setReadonly(true)\n/// }\n/// }\n/// ```\nexport function useCrepe(ctx: Ctx) {\n // We should use string slice here to avoid the slice to be bundled in multiple entries\n return ctx.get<CrepeBuilder, 'CrepeCtx'>('CrepeCtx')\n}\n\n/// Check the enabled FeatureFlags\n/// ```ts\n/// import { useCrepeFeatures } from '@milkdown/crepe'\n/// const plugin = (ctx: Ctx) => {\n/// const features = useCrepeFeatures(ctx)\n/// if (features.get().includes(CrepeFeature.CodeMirror)) {\n/// // Do something with CodeMirror\n/// }\n/// }\nexport function useCrepeFeatures(ctx: Ctx) {\n // We should use string slice here to avoid the slice to be bundled in multiple entries\n return ctx.use<CrepeFeature[], 'FeaturesCtx'>('FeaturesCtx')\n}\n\n/// @internal\nexport function crepeFeatureConfig(feature: CrepeFeature) {\n return (ctx: Ctx) => {\n useCrepeFeatures(ctx).update((features) => {\n if (features.includes(feature)) {\n return features\n }\n return [...features, feature]\n })\n }\n}\n","import { imageBlockConfig } from '@milkdown/kit/component/image-block'\nimport {\n type DefaultValue,\n defaultValueCtx,\n Editor,\n EditorStatus,\n editorViewCtx,\n editorViewOptionsCtx,\n rootCtx,\n} from '@milkdown/kit/core'\nimport { clipboard } from '@milkdown/kit/plugin/clipboard'\nimport { history } from '@milkdown/kit/plugin/history'\nimport { indent, indentConfig } from '@milkdown/kit/plugin/indent'\nimport {\n listener,\n listenerCtx,\n type ListenerManager,\n} from '@milkdown/kit/plugin/listener'\nimport { trailing } from '@milkdown/kit/plugin/trailing'\nimport { upload, uploadConfig } from '@milkdown/kit/plugin/upload'\nimport { commonmark } from '@milkdown/kit/preset/commonmark'\nimport { gfm } from '@milkdown/kit/preset/gfm'\nimport { getMarkdown } from '@milkdown/kit/utils'\n\nimport type { CrepeFeatureConfig } from '../feature'\nimport type { DefineFeature } from '../feature/shared'\n\nimport { CrepeFeature } from '../feature'\nimport { CrepeCtx, FeaturesCtx, useCrepeFeatures } from './slice'\n\n/// The crepe builder configuration.\nexport interface CrepeBuilderConfig {\n /// The root element for the editor.\n /// Supports both DOM nodes and CSS selectors,\n /// If not provided, the editor will be appended to the body.\n root?: Node | string | null\n\n /// The default value for the editor.\n defaultValue?: DefaultValue\n}\n\n/// The crepe builder class.\n/// This class allows users to manually add features to the editor.\nexport class CrepeBuilder {\n /// @internal\n readonly #editor: Editor\n\n /// @internal\n readonly #rootElement: Node\n\n /// @internal\n #editable = true\n\n /// The constructor of the crepe builder.\n /// You can pass configs to the builder to configure the editor.\n constructor({ root, defaultValue = '' }: CrepeBuilderConfig = {}) {\n this.#rootElement =\n (typeof root === 'string' ? document.querySelector(root) : root) ??\n document.body\n this.#editor = Editor.make()\n .config((ctx) => {\n ctx.inject(CrepeCtx, this)\n ctx.inject(FeaturesCtx, [])\n })\n .config((ctx) => {\n ctx.set(rootCtx, this.#rootElement)\n ctx.set(defaultValueCtx, defaultValue)\n ctx.set(editorViewOptionsCtx, {\n editable: () => this.#editable,\n })\n ctx.update(indentConfig.key, (value) => ({\n ...value,\n size: 4,\n }))\n ctx.update(uploadConfig.key, (prev) => ({\n ...prev,\n uploader: async (files, schema, ctx) => {\n const features = useCrepeFeatures(ctx).get()\n const hasImageBlock = features.includes(CrepeFeature.ImageBlock)\n const nodeType = hasImageBlock\n ? schema.nodes['image-block']\n : schema.nodes['image']\n\n if (!nodeType) return []\n\n const onUpload = hasImageBlock\n ? ctx.get(imageBlockConfig.key).onUpload\n : undefined\n\n const images: File[] = []\n for (let i = 0; i < files.length; i++) {\n const file = files.item(i)\n if (file && file.type.includes('image')) images.push(file)\n }\n\n const nodes = await Promise.all(\n images.map(async (file) => {\n const src = onUpload\n ? await onUpload(file)\n : URL.createObjectURL(file)\n return nodeType.createAndFill({ src })!\n })\n )\n\n return nodes\n },\n }))\n })\n .use(commonmark)\n .use(listener)\n .use(history)\n .use(indent)\n .use(trailing)\n .use(clipboard)\n .use(upload)\n .use(gfm)\n }\n\n /// Add a feature to the editor.\n addFeature: {\n <T extends CrepeFeature>(\n feature: DefineFeature<CrepeFeatureConfig[T]>,\n config?: CrepeFeatureConfig[T]\n ): CrepeBuilder\n <C>(feature: DefineFeature<C>, config?: C): CrepeBuilder\n } = (feature: DefineFeature, config?: never) => {\n feature(this.#editor, config)\n return this\n }\n\n /// Create the editor.\n create = () => {\n return this.#editor.create()\n }\n\n /// Destroy the editor.\n destroy = () => {\n return this.#editor.destroy()\n }\n\n /// Get the milkdown editor instance.\n get editor(): Editor {\n return this.#editor\n }\n\n /// Get the readonly state of the editor.\n get readonly() {\n return !this.#editable\n }\n\n /// Set the readonly mode of the editor.\n setReadonly = (value: boolean) => {\n this.#editable = !value\n this.#editor.action((ctx) => {\n if (this.#editor.status === EditorStatus.Created) {\n const view = ctx.get(editorViewCtx)\n view.setProps({\n editable: () => !value,\n })\n }\n })\n return this\n }\n\n /// Get the markdown content of the editor.\n getMarkdown = () => {\n return this.#editor.action(getMarkdown())\n }\n\n /// Register event listeners.\n on = (fn: (api: ListenerManager) => void) => {\n if (this.#editor.status !== EditorStatus.Created) {\n this.#editor.config((ctx) => {\n const listener = ctx.get(listenerCtx)\n fn(listener)\n })\n return this\n }\n this.#editor.action((ctx) => {\n const listener = ctx.get(listenerCtx)\n fn(listener)\n })\n return this\n }\n}\n"],"names":["CrepeFeature","listener","ctx"],"mappings":"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"}
|