@cellgit/markdown-render 0.1.0 → 1.0.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/README.md +436 -178
- package/README.zh-CN.md +530 -0
- package/THIRD-PARTY-NOTICES.md +332 -0
- package/dist/markdown-render.css +3 -3
- package/dist/markdown-render.esm.css +3 -3
- package/dist/markdown-render.esm.js +1 -91452
- package/dist/markdown-render.html +20 -75
- package/dist/markdown-render.js +1 -91457
- package/dist/scripts/bridge.js +119 -0
- package/dist/scripts/chat-renderer.js +2770 -0
- package/dist/scripts/copy.js +155 -0
- package/dist/scripts/height-sync.js +205 -0
- package/dist/scripts/renderer.js +398 -0
- package/ios-example/MarkdownViewController.swift +123 -33
- package/ios-example/README.md +2 -0
- package/ios-example/SwiftUIMarkdownExample.swift +111 -0
- package/package.json +17 -9
- package/dist/fonts/KaTeX_AMS-Regular.ttf +0 -0
- package/dist/fonts/KaTeX_AMS-Regular.woff +0 -0
- package/dist/fonts/KaTeX_Caligraphic-Bold.ttf +0 -0
- package/dist/fonts/KaTeX_Caligraphic-Bold.woff +0 -0
- package/dist/fonts/KaTeX_Caligraphic-Regular.ttf +0 -0
- package/dist/fonts/KaTeX_Caligraphic-Regular.woff +0 -0
- package/dist/fonts/KaTeX_Fraktur-Bold.ttf +0 -0
- package/dist/fonts/KaTeX_Fraktur-Bold.woff +0 -0
- package/dist/fonts/KaTeX_Fraktur-Regular.ttf +0 -0
- package/dist/fonts/KaTeX_Fraktur-Regular.woff +0 -0
- package/dist/fonts/KaTeX_Main-Bold.ttf +0 -0
- package/dist/fonts/KaTeX_Main-Bold.woff +0 -0
- package/dist/fonts/KaTeX_Main-BoldItalic.ttf +0 -0
- package/dist/fonts/KaTeX_Main-BoldItalic.woff +0 -0
- package/dist/fonts/KaTeX_Main-Italic.ttf +0 -0
- package/dist/fonts/KaTeX_Main-Italic.woff +0 -0
- package/dist/fonts/KaTeX_Main-Regular.ttf +0 -0
- package/dist/fonts/KaTeX_Main-Regular.woff +0 -0
- package/dist/fonts/KaTeX_Math-BoldItalic.ttf +0 -0
- package/dist/fonts/KaTeX_Math-BoldItalic.woff +0 -0
- package/dist/fonts/KaTeX_Math-Italic.ttf +0 -0
- package/dist/fonts/KaTeX_Math-Italic.woff +0 -0
- package/dist/fonts/KaTeX_SansSerif-Bold.ttf +0 -0
- package/dist/fonts/KaTeX_SansSerif-Bold.woff +0 -0
- package/dist/fonts/KaTeX_SansSerif-Italic.ttf +0 -0
- package/dist/fonts/KaTeX_SansSerif-Italic.woff +0 -0
- package/dist/fonts/KaTeX_SansSerif-Regular.ttf +0 -0
- package/dist/fonts/KaTeX_SansSerif-Regular.woff +0 -0
- package/dist/fonts/KaTeX_Script-Regular.ttf +0 -0
- package/dist/fonts/KaTeX_Script-Regular.woff +0 -0
- package/dist/fonts/KaTeX_Size1-Regular.ttf +0 -0
- package/dist/fonts/KaTeX_Size1-Regular.woff +0 -0
- package/dist/fonts/KaTeX_Size2-Regular.ttf +0 -0
- package/dist/fonts/KaTeX_Size2-Regular.woff +0 -0
- package/dist/fonts/KaTeX_Size3-Regular.ttf +0 -0
- package/dist/fonts/KaTeX_Size3-Regular.woff +0 -0
- package/dist/fonts/KaTeX_Size4-Regular.ttf +0 -0
- package/dist/fonts/KaTeX_Size4-Regular.woff +0 -0
- package/dist/fonts/KaTeX_Typewriter-Regular.ttf +0 -0
- package/dist/fonts/KaTeX_Typewriter-Regular.woff +0 -0
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
(function (global) {
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const DEFAULT_STRINGS = {
|
|
5
|
+
copy: 'Copy',
|
|
6
|
+
copied: 'Copied',
|
|
7
|
+
copyFailed: 'Copy failed'
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
function resolveStrings(overrides) {
|
|
11
|
+
return Object.assign({}, DEFAULT_STRINGS, overrides || {});
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Copy text using the modern Clipboard API when available; fall back to
|
|
16
|
+
* a transient textarea + execCommand for older WebKit / WKWebView combos.
|
|
17
|
+
* Both paths run inside the click handler so we keep the user-gesture
|
|
18
|
+
* context required by Safari.
|
|
19
|
+
*/
|
|
20
|
+
function copyTextToClipboard(text) {
|
|
21
|
+
if (!text) return Promise.reject(new Error('Nothing to copy'));
|
|
22
|
+
|
|
23
|
+
const nav = global.navigator;
|
|
24
|
+
if (nav && nav.clipboard && typeof nav.clipboard.writeText === 'function') {
|
|
25
|
+
return nav.clipboard.writeText(text).catch(() => fallbackCopy(text));
|
|
26
|
+
}
|
|
27
|
+
return fallbackCopy(text);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function fallbackCopy(text) {
|
|
31
|
+
return new Promise((resolve, reject) => {
|
|
32
|
+
try {
|
|
33
|
+
const textarea = global.document.createElement('textarea');
|
|
34
|
+
textarea.value = text;
|
|
35
|
+
textarea.setAttribute('readonly', '');
|
|
36
|
+
textarea.style.position = 'fixed';
|
|
37
|
+
textarea.style.top = '-9999px';
|
|
38
|
+
textarea.style.opacity = '0';
|
|
39
|
+
global.document.body.appendChild(textarea);
|
|
40
|
+
textarea.focus();
|
|
41
|
+
textarea.select();
|
|
42
|
+
const succeeded = global.document.execCommand('copy');
|
|
43
|
+
global.document.body.removeChild(textarea);
|
|
44
|
+
if (succeeded) resolve();
|
|
45
|
+
else reject(new Error('execCommand copy failed'));
|
|
46
|
+
} catch (err) {
|
|
47
|
+
reject(err);
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function findActionable(event) {
|
|
53
|
+
let node = event.target;
|
|
54
|
+
while (node) {
|
|
55
|
+
if (node.dataset && node.dataset.action === 'copy') return node;
|
|
56
|
+
node = node.parentElement;
|
|
57
|
+
}
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function findWrapper(node) {
|
|
62
|
+
while (node) {
|
|
63
|
+
if (node.classList && node.classList.contains('code-block-wrapper')) return node;
|
|
64
|
+
node = node.parentElement;
|
|
65
|
+
}
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function attachCopyHandler(options) {
|
|
70
|
+
const opts = options || {};
|
|
71
|
+
if (!global.document) return () => {};
|
|
72
|
+
|
|
73
|
+
const bridge = opts.bridge || global.MarkdownBridge;
|
|
74
|
+
const strings = resolveStrings(opts.strings);
|
|
75
|
+
|
|
76
|
+
function resetButton(button, textNode, originalText) {
|
|
77
|
+
if (textNode) textNode.textContent = originalText;
|
|
78
|
+
if (button) button.classList.remove('copied');
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function handleCopy(event) {
|
|
82
|
+
const actionable = findActionable(event);
|
|
83
|
+
if (!actionable) return;
|
|
84
|
+
|
|
85
|
+
const wrapper = findWrapper(actionable);
|
|
86
|
+
if (!wrapper) return;
|
|
87
|
+
|
|
88
|
+
const codeNode = wrapper.querySelector('pre code');
|
|
89
|
+
if (!codeNode) return;
|
|
90
|
+
|
|
91
|
+
const codeText = codeNode.textContent || '';
|
|
92
|
+
if (!codeText) return;
|
|
93
|
+
|
|
94
|
+
// Prevent the click from bubbling into other UI listeners.
|
|
95
|
+
event.preventDefault();
|
|
96
|
+
|
|
97
|
+
const button = wrapper.querySelector('.code-copy-button');
|
|
98
|
+
const copyTextSpan = button ? button.querySelector('.copy-text') : null;
|
|
99
|
+
const originalText = copyTextSpan ? copyTextSpan.textContent || strings.copy : strings.copy;
|
|
100
|
+
const languageLabel = wrapper.querySelector('.code-language');
|
|
101
|
+
const language = languageLabel ? languageLabel.textContent || '' : '';
|
|
102
|
+
const message = wrapper.closest ? wrapper.closest('[data-message-id]') : null;
|
|
103
|
+
|
|
104
|
+
const payload = {
|
|
105
|
+
text: codeText,
|
|
106
|
+
language,
|
|
107
|
+
source: 'code-block',
|
|
108
|
+
timestamp: Date.now()
|
|
109
|
+
};
|
|
110
|
+
if (message) {
|
|
111
|
+
payload.messageId = message.getAttribute('data-message-id') || '';
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
copyTextToClipboard(codeText)
|
|
115
|
+
.then(() => {
|
|
116
|
+
if (copyTextSpan) copyTextSpan.textContent = strings.copied;
|
|
117
|
+
if (button) button.classList.add('copied');
|
|
118
|
+
setTimeout(() => resetButton(button, copyTextSpan, originalText), 2000);
|
|
119
|
+
if (bridge && typeof bridge.notifyCopyResult === 'function') {
|
|
120
|
+
bridge.notifyCopyResult({ success: true, dict: payload });
|
|
121
|
+
}
|
|
122
|
+
})
|
|
123
|
+
.catch((error) => {
|
|
124
|
+
console.warn('Failed to copy code:', error);
|
|
125
|
+
// Say it failed before going back to the resting label. Snapping
|
|
126
|
+
// straight back reads as "nothing happened", which is the one thing
|
|
127
|
+
// that did not happen.
|
|
128
|
+
if (copyTextSpan) copyTextSpan.textContent = strings.copyFailed;
|
|
129
|
+
setTimeout(() => resetButton(button, copyTextSpan, originalText), 2000);
|
|
130
|
+
if (bridge && typeof bridge.notifyCopyResult === 'function') {
|
|
131
|
+
bridge.notifyCopyResult({
|
|
132
|
+
success: false,
|
|
133
|
+
error: error && error.message ? error.message : String(error),
|
|
134
|
+
dict: payload
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// No keydown handler: the only thing carrying data-action="copy" is a real
|
|
141
|
+
// <button>, and the platform already turns Enter and Space on one into a
|
|
142
|
+
// click. Handling keydown as well would fire the copy twice unless the
|
|
143
|
+
// default were suppressed exactly right.
|
|
144
|
+
global.document.addEventListener('click', handleCopy);
|
|
145
|
+
|
|
146
|
+
return () => {
|
|
147
|
+
global.document.removeEventListener('click', handleCopy);
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
global.MarkdownCopy = {
|
|
152
|
+
attachCopyHandler,
|
|
153
|
+
resolveStrings
|
|
154
|
+
};
|
|
155
|
+
})(window);
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
(function (global) {
|
|
2
|
+
const HEIGHT_EPS = 0.5;
|
|
3
|
+
|
|
4
|
+
function now() {
|
|
5
|
+
return typeof performance !== 'undefined' && performance.now
|
|
6
|
+
? performance.now()
|
|
7
|
+
: Date.now();
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function raf(callback) {
|
|
11
|
+
if (typeof global.requestAnimationFrame === 'function') {
|
|
12
|
+
return global.requestAnimationFrame(callback);
|
|
13
|
+
}
|
|
14
|
+
return setTimeout(callback, 16);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function caf(id) {
|
|
18
|
+
if (!id) return;
|
|
19
|
+
if (typeof global.cancelAnimationFrame === 'function') {
|
|
20
|
+
global.cancelAnimationFrame(id);
|
|
21
|
+
} else {
|
|
22
|
+
clearTimeout(id);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function measureHeight(container) {
|
|
27
|
+
const bodyHeight = global.document && global.document.body
|
|
28
|
+
? global.document.body.scrollHeight
|
|
29
|
+
: 0;
|
|
30
|
+
const containerHeight = container ? container.scrollHeight : 0;
|
|
31
|
+
return Math.max(bodyHeight, containerHeight);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function createHeightSync(options = {}) {
|
|
35
|
+
const documentRef = global.document;
|
|
36
|
+
const container = options.container || (documentRef ? documentRef.getElementById('markdown-content') : null);
|
|
37
|
+
const bridge = options.bridge || global.MarkdownBridge;
|
|
38
|
+
const threshold = typeof options.threshold === 'number' ? options.threshold : HEIGHT_EPS;
|
|
39
|
+
const cleanups = [];
|
|
40
|
+
let lastKnownHeight = -1;
|
|
41
|
+
let rafId = null;
|
|
42
|
+
let hasPostedInitial = false;
|
|
43
|
+
|
|
44
|
+
function dispatch(reason, dispatchOptions = {}) {
|
|
45
|
+
const height = measureHeight(container);
|
|
46
|
+
if (!dispatchOptions.force && Math.abs(height - lastKnownHeight) < threshold) {
|
|
47
|
+
return height;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
lastKnownHeight = height;
|
|
51
|
+
|
|
52
|
+
if (dispatchOptions.initial) {
|
|
53
|
+
hasPostedInitial = true;
|
|
54
|
+
if (bridge && typeof bridge.notifyRenderComplete === 'function') {
|
|
55
|
+
bridge.notifyRenderComplete({
|
|
56
|
+
success: true,
|
|
57
|
+
height,
|
|
58
|
+
reason: reason || 'render'
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
if (typeof options.onInitialRender === 'function') {
|
|
62
|
+
options.onInitialRender(height);
|
|
63
|
+
}
|
|
64
|
+
} else if (bridge && typeof bridge.notifyContentHeight === 'function') {
|
|
65
|
+
const handled = bridge.notifyContentHeight(
|
|
66
|
+
height,
|
|
67
|
+
reason || 'content-change'
|
|
68
|
+
);
|
|
69
|
+
if (!handled && typeof bridge.notifyRenderComplete === 'function') {
|
|
70
|
+
bridge.notifyRenderComplete({
|
|
71
|
+
success: true,
|
|
72
|
+
height,
|
|
73
|
+
reason: reason || 'content-change'
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return height;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function schedule(reason, dispatchOptions = {}) {
|
|
82
|
+
if (rafId) {
|
|
83
|
+
caf(rafId);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
rafId = raf(() => {
|
|
87
|
+
rafId = null;
|
|
88
|
+
dispatch(reason, dispatchOptions);
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function beginRenderCycle() {
|
|
93
|
+
hasPostedInitial = false;
|
|
94
|
+
lastKnownHeight = -1;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function setupResizeObserver() {
|
|
98
|
+
if (!container || typeof global.ResizeObserver !== 'function') {
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const resizeObserver = new ResizeObserver(() => {
|
|
103
|
+
schedule('resize-observer');
|
|
104
|
+
});
|
|
105
|
+
resizeObserver.observe(container);
|
|
106
|
+
|
|
107
|
+
cleanups.push(() => resizeObserver.disconnect());
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function setupMutationObserver() {
|
|
111
|
+
if (!container || typeof global.MutationObserver !== 'function') {
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const mutationObserver = new MutationObserver(() => {
|
|
116
|
+
schedule('mutation');
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
// characterData is intentionally NOT observed. Text-only mutations
|
|
120
|
+
// (e.g. KaTeX inner spans being rewritten) do not change container
|
|
121
|
+
// height, and observing them firehoses the bridge during streaming.
|
|
122
|
+
mutationObserver.observe(container, {
|
|
123
|
+
childList: true,
|
|
124
|
+
subtree: true
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
cleanups.push(() => mutationObserver.disconnect());
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function setupViewportListeners() {
|
|
131
|
+
if (typeof global.addEventListener !== 'function') {
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const onResize = () => schedule('viewport-resize', { force: true });
|
|
136
|
+
const onOrientation = () => schedule('orientationchange', { force: true });
|
|
137
|
+
const onResourceLoad = () => schedule('resource-load');
|
|
138
|
+
|
|
139
|
+
global.addEventListener('resize', onResize);
|
|
140
|
+
global.addEventListener('orientationchange', onOrientation);
|
|
141
|
+
if (documentRef) {
|
|
142
|
+
documentRef.addEventListener('load', onResourceLoad, true);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
cleanups.push(() => {
|
|
146
|
+
global.removeEventListener('resize', onResize);
|
|
147
|
+
global.removeEventListener('orientationchange', onOrientation);
|
|
148
|
+
if (documentRef) {
|
|
149
|
+
documentRef.removeEventListener('load', onResourceLoad, true);
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function setupLifecycleCleanup() {
|
|
155
|
+
if (typeof global.addEventListener !== 'function') {
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const cleanup = () => {
|
|
160
|
+
disconnect();
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
global.addEventListener('pagehide', cleanup, { once: true });
|
|
164
|
+
global.addEventListener('beforeunload', cleanup, { once: true });
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function disconnect() {
|
|
168
|
+
cleanups.splice(0).forEach((fn) => {
|
|
169
|
+
try {
|
|
170
|
+
fn();
|
|
171
|
+
} catch (err) {
|
|
172
|
+
console.warn('Failed to cleanup height sync listener', err);
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
if (rafId) {
|
|
176
|
+
caf(rafId);
|
|
177
|
+
rafId = null;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
setupResizeObserver();
|
|
182
|
+
setupMutationObserver();
|
|
183
|
+
setupViewportListeners();
|
|
184
|
+
setupLifecycleCleanup();
|
|
185
|
+
|
|
186
|
+
return {
|
|
187
|
+
measure: () => measureHeight(container),
|
|
188
|
+
scheduleInitial: (reason) => schedule(reason, { initial: true, force: true }),
|
|
189
|
+
scheduleHeight: (reason, opts) => schedule(reason, opts || {}),
|
|
190
|
+
dispatch,
|
|
191
|
+
beginRenderCycle,
|
|
192
|
+
hasPostedInitial: () => hasPostedInitial,
|
|
193
|
+
disconnect,
|
|
194
|
+
debugState: () => ({
|
|
195
|
+
lastKnownHeight,
|
|
196
|
+
hasPostedInitial,
|
|
197
|
+
timestamp: now()
|
|
198
|
+
})
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
global.MarkdownHeightSync = {
|
|
203
|
+
createHeightSync
|
|
204
|
+
};
|
|
205
|
+
})(window);
|