@lyhue1991/dsh-soup 0.6.0 → 0.7.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 +2 -2
- package/lib/client/explorer-view.js +1 -1
- package/lib/client/file-address-bridge.js +68 -0
- package/lib/client/html-preview.js +120 -1
- package/lib/client/preview-renderers.js +51 -22
- package/lib/client.entry.js +1 -0
- package/lib/client.js +319 -94
- package/lib/client.main.js +38 -26
- package/package.json +4 -3
package/README.md
CHANGED
|
@@ -24,7 +24,7 @@ DSH(DeepSeek Harness)功能很强:会话、工具、轨迹、Goal 一应
|
|
|
24
24
|
|
|
25
25
|
### 🧅 右侧文件树(主打 · 葱 · 提鲜点缀)
|
|
26
26
|
|
|
27
|
-
一个真正和左侧工作区并列的文件浏览器——不悬浮、不遮挡,配色与宽度都与左侧一致(默认
|
|
27
|
+
一个真正和左侧工作区并列的文件浏览器——不悬浮、不遮挡,配色与宽度都与左侧一致(默认 300px,拖左缘在 300–520px 间调整)。
|
|
28
28
|
|
|
29
29
|
- **跟随会话**:自动展示当前会话的工作目录,切会话/工作区时跟着走。
|
|
30
30
|
- **右键菜单**:预览、用系统打开、重命名、移到废纸篓、复制路径、下载;目录多出新建文件、新建文件夹、上传。
|
|
@@ -50,7 +50,7 @@ DSH(DeepSeek Harness)功能很强:会话、工具、轨迹、Goal 一应
|
|
|
50
50
|
- **Notebook(.ipynb)**:GitHub 风格静态渲染(代码高亮 + 图片/输出内联)。
|
|
51
51
|
- **图片**:png/jpg/gif/webp/svg 等内联展示(≤8MB)。
|
|
52
52
|
- **其余**:二进制给出提示,超大文本截断为纯文本。
|
|
53
|
-
- **文本编辑**:Markdown / Python / JavaScript / TypeScript / JSON / HTML / CSS / Shell 等常见文本文件可点「编辑」,支持 ⌘/Ctrl+S 立即保存、未保存标记与返回预览;Python / JSON 等纯代码文件在编辑态保留与预览一致的左侧行号与语法配色,Markdown 返回预览时展示当前草稿。
|
|
53
|
+
- **文本编辑**:Markdown / Python / JavaScript / TypeScript / JSON / HTML / CSS / Shell,以及 `.gitignore`、`Dockerfile`、`Makefile`、`LICENSE` 等常见文本文件可点「编辑」,支持 ⌘/Ctrl+S 立即保存、未保存标记与返回预览;Python / JSON 等纯代码文件在编辑态保留与预览一致的左侧行号与语法配色,Markdown 返回预览时展示当前草稿。
|
|
54
54
|
- **自动保存**:参考 VS Code `files.autoSaveDelay`,停止输入 5 秒后自动保存;退出编辑时也会保存。`⌘S / Ctrl+S` 保存成功后工具栏会短暂显示“已保存”。编辑态默认覆盖磁盘上的其他更改,工具栏不显示保存/取消按钮。
|
|
55
55
|
- **跟随会话**:切到别的项目时,范围外的预览自动关闭。
|
|
56
56
|
- **自动重读**:正在看的文件被外部(agent / 编辑器)修改时,预览内容几秒内自动更新;若你正在编辑,则保留草稿并提示冲突。
|
|
@@ -20,7 +20,7 @@ export function createExplorerView(ctx) {
|
|
|
20
20
|
var uploadTargetDir = null
|
|
21
21
|
var DETAILS_MIN = 300
|
|
22
22
|
var DETAILS_MAX = 520
|
|
23
|
-
var DETAILS_DEFAULT =
|
|
23
|
+
var DETAILS_DEFAULT = 300
|
|
24
24
|
function clampW(w) { return Math.max(DETAILS_MIN, Math.min(DETAILS_MAX, w)) }
|
|
25
25
|
|
|
26
26
|
// Hook 规则安全兜底:宿主未注入 useSessions 时也必须调用恰好一个 hook,
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { fileAddressMatches } from './file-address.js'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Wrap the official Sidebar resource opener so file resources keep working
|
|
5
|
+
* when dsh-soup owns the right-column surface. The service is intentionally
|
|
6
|
+
* passed in by the caller: it can be provided after this plugin's apply()
|
|
7
|
+
* lifecycle has already started, and may be replaced on a host reconnect.
|
|
8
|
+
*/
|
|
9
|
+
export function bridgeFileAddressOpener(service, ctx) {
|
|
10
|
+
if (!service || typeof service.openResource !== 'function') return null
|
|
11
|
+
if (service.__dshSoupFileBridge) return null
|
|
12
|
+
|
|
13
|
+
var originalOpenResource = service.openResource
|
|
14
|
+
var originalOpenResourceIn = service.openResourceIn
|
|
15
|
+
var openFileAddressInSoup = ctx.openFileAddressInSoup
|
|
16
|
+
var getActiveSessionId = ctx.getActiveSessionId
|
|
17
|
+
|
|
18
|
+
function openInSoup(address, sessionIdHint) {
|
|
19
|
+
if (!fileAddressMatches(address, sessionIdHint, getActiveSessionId())) return false
|
|
20
|
+
try {
|
|
21
|
+
var task = openFileAddressInSoup(address, sessionIdHint)
|
|
22
|
+
// Sidebar actions are synchronous. Keep asynchronous preview failures
|
|
23
|
+
// from becoming unhandled rejections in the host click handler.
|
|
24
|
+
if (task && typeof task.catch === 'function') task.catch(function () {})
|
|
25
|
+
return true
|
|
26
|
+
} catch (err) {
|
|
27
|
+
return false
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
var bridgedOpenResource = function (address, options) {
|
|
32
|
+
if (openInSoup(address, null)) return undefined
|
|
33
|
+
return originalOpenResource.call(this, address, options)
|
|
34
|
+
}
|
|
35
|
+
var bridgedOpenResourceIn = typeof originalOpenResourceIn === 'function'
|
|
36
|
+
? function (sessionId, address, options) {
|
|
37
|
+
if (openInSoup(address, sessionId)) return undefined
|
|
38
|
+
return originalOpenResourceIn.call(this, sessionId, address, options)
|
|
39
|
+
}
|
|
40
|
+
: null
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
service.openResource = bridgedOpenResource
|
|
44
|
+
if (bridgedOpenResourceIn) service.openResourceIn = bridgedOpenResourceIn
|
|
45
|
+
if (service.openResource !== bridgedOpenResource ||
|
|
46
|
+
(bridgedOpenResourceIn && service.openResourceIn !== bridgedOpenResourceIn)) return null
|
|
47
|
+
try { Object.defineProperty(service, '__dshSoupFileBridge', { value: true, configurable: true }) }
|
|
48
|
+
catch (err) { service.__dshSoupFileBridge = true }
|
|
49
|
+
} catch (err) {
|
|
50
|
+
try {
|
|
51
|
+
if (service.openResource === bridgedOpenResource) service.openResource = originalOpenResource
|
|
52
|
+
if (bridgedOpenResourceIn && service.openResourceIn === bridgedOpenResourceIn) {
|
|
53
|
+
service.openResourceIn = originalOpenResourceIn
|
|
54
|
+
}
|
|
55
|
+
} catch (restoreErr) {}
|
|
56
|
+
return null
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return function () {
|
|
60
|
+
if (service.openResource === bridgedOpenResource) service.openResource = originalOpenResource
|
|
61
|
+
if (bridgedOpenResourceIn && service.openResourceIn === bridgedOpenResourceIn) {
|
|
62
|
+
service.openResourceIn = originalOpenResourceIn
|
|
63
|
+
}
|
|
64
|
+
try { delete service.__dshSoupFileBridge } catch (err) {}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (typeof window !== 'undefined') window.__DSH_SOUP_FILE_ADDRESS_BRIDGE__ = { bridgeFileAddressOpener }
|
|
@@ -7,6 +7,13 @@
|
|
|
7
7
|
const ASSET_MAX_BYTES = 4 * 1024 * 1024
|
|
8
8
|
const TOTAL_MAX_BYTES = 32 * 1024 * 1024
|
|
9
9
|
const MAX_ASSETS = 64
|
|
10
|
+
const IMAGE_MAX_COUNT = 64
|
|
11
|
+
/** Same whitelist as the host inline-image route (host/file-view.js IMAGE_MIME). */
|
|
12
|
+
const IMAGE_DATA_MIME = {
|
|
13
|
+
png: 'image/png', jpg: 'image/jpeg', jpeg: 'image/jpeg', gif: 'image/gif',
|
|
14
|
+
webp: 'image/webp', svg: 'image/svg+xml', bmp: 'image/bmp', ico: 'image/x-icon',
|
|
15
|
+
}
|
|
16
|
+
const CSS_URL_RE = /url\(\s*('([^']*)'|"([^"]*)"|([^)'"]*))\s*\)/gi
|
|
10
17
|
|
|
11
18
|
function base64ToText(data) {
|
|
12
19
|
const raw = atob(data)
|
|
@@ -27,6 +34,21 @@ function assetPath(reference) {
|
|
|
27
34
|
return cut === -1 ? String(reference || '') : String(reference || '').slice(0, cut)
|
|
28
35
|
}
|
|
29
36
|
|
|
37
|
+
function imageMimeOf(reference) {
|
|
38
|
+
const match = /\.([A-Za-z0-9]+)$/.exec(assetPath(reference))
|
|
39
|
+
return match ? IMAGE_DATA_MIME[match[1].toLowerCase()] : undefined
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function relativeKey(reference) {
|
|
43
|
+
const out = []
|
|
44
|
+
for (const segment of String(reference || '').split('/')) {
|
|
45
|
+
if (segment === '' || segment === '.') continue
|
|
46
|
+
if (segment === '..') out.pop()
|
|
47
|
+
else out.push(segment)
|
|
48
|
+
}
|
|
49
|
+
return out.join('/')
|
|
50
|
+
}
|
|
51
|
+
|
|
30
52
|
/** Preserve legacy WeChat document compatibility in every rendering mode. */
|
|
31
53
|
export function prepareHtmlText(text) {
|
|
32
54
|
let next = String(text || '')
|
|
@@ -100,7 +122,104 @@ export async function packHtmlPreview({ text, rootPath, sessionId, rpc, signal }
|
|
|
100
122
|
assets.push({ kind, reference, text: assetText })
|
|
101
123
|
} catch { /* malformed UTF-8 dependency is skipped */ }
|
|
102
124
|
}
|
|
103
|
-
|
|
125
|
+
|
|
126
|
+
// ------------------------------------------------------------------
|
|
127
|
+
// 相对路径图片内联为 data URL。srcdoc iframe 的文档 base URL 继承宿主
|
|
128
|
+
// 应用页面,`article_files/img.jpg` 之类的相对 <img> 会被解析到应用自身
|
|
129
|
+
// 路由而 404;且沙箱(无 allow-same-origin)发出的子资源请求 Origin 为
|
|
130
|
+
// opaque,无法走 /api/dsh-soup/img 的同源校验。因此与 script/css 同路:
|
|
131
|
+
// 经 read-related(限定文档目录内)读出后内联,彻底不依赖网络解析。
|
|
132
|
+
// ------------------------------------------------------------------
|
|
133
|
+
const imageCache = new Map()
|
|
134
|
+
function loadImage(reference) {
|
|
135
|
+
let bare = assetPath(reference)
|
|
136
|
+
try { bare = decodeURIComponent(bare) } catch { /* keep raw path */ }
|
|
137
|
+
if (!imageMimeOf(bare)) return Promise.resolve(null)
|
|
138
|
+
const key = relativeKey(bare)
|
|
139
|
+
let entry = imageCache.get(key)
|
|
140
|
+
if (!entry) {
|
|
141
|
+
if (imageCache.size >= IMAGE_MAX_COUNT) return Promise.resolve(null)
|
|
142
|
+
entry = Promise.resolve(rpc('read-related', { path: rootPath, relativePath: bare, sessionId }))
|
|
143
|
+
.then((response) => {
|
|
144
|
+
if (!response?.ok || response.size > ASSET_MAX_BYTES) return null
|
|
145
|
+
const bytes = Math.ceil(response.data.length * 3 / 4)
|
|
146
|
+
if (total + bytes > TOTAL_MAX_BYTES) return null
|
|
147
|
+
total += bytes
|
|
148
|
+
return { url: 'data:' + imageMimeOf(bare) + ';base64,' + response.data, bytes }
|
|
149
|
+
})
|
|
150
|
+
.catch(() => null)
|
|
151
|
+
imageCache.set(key, entry)
|
|
152
|
+
}
|
|
153
|
+
return entry
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
let mutated = false
|
|
157
|
+
await Promise.all(
|
|
158
|
+
Array.from(template.content.querySelectorAll('img[src],source[src],video[poster],input[type="image"][src]'))
|
|
159
|
+
.map(async (element) => {
|
|
160
|
+
const attribute = element.localName === 'video' ? 'poster' : 'src'
|
|
161
|
+
const reference = element.getAttribute(attribute) || ''
|
|
162
|
+
if (!relativeReference(reference) || !imageMimeOf(reference)) return
|
|
163
|
+
signal.throwIfAborted?.()
|
|
164
|
+
const image = await loadImage(reference)
|
|
165
|
+
if (image) { element.setAttribute(attribute, image.url); mutated = true }
|
|
166
|
+
}),
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
// 响应式图片:srcset 是「url 描述符, url 描述符」列表,逐候选改写。
|
|
170
|
+
for (const element of template.content.querySelectorAll('img[srcset],source[srcset]')) {
|
|
171
|
+
signal.throwIfAborted?.()
|
|
172
|
+
const value = element.getAttribute('srcset') || ''
|
|
173
|
+
if (!value) continue
|
|
174
|
+
const parts = await Promise.all(value.split(',').map(async (part) => {
|
|
175
|
+
const trimmed = part.trim()
|
|
176
|
+
if (!trimmed) return part
|
|
177
|
+
const cut = trimmed.search(/\s/)
|
|
178
|
+
const url = cut === -1 ? trimmed : trimmed.slice(0, cut)
|
|
179
|
+
const rest = cut === -1 ? '' : trimmed.slice(cut)
|
|
180
|
+
if (!relativeReference(url)) return part
|
|
181
|
+
const image = await loadImage(url)
|
|
182
|
+
return image ? image.url + rest : part
|
|
183
|
+
}))
|
|
184
|
+
const next = parts.join(',')
|
|
185
|
+
if (next !== value) { element.setAttribute('srcset', next); mutated = true }
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// 内联 <style> 与 style 属性里的相对 url(...) 背景图(仅图片扩展名)。
|
|
189
|
+
async function rewriteCssUrls(text) {
|
|
190
|
+
if (!text || String(text).indexOf('url(') === -1) return text
|
|
191
|
+
const refs = new Set()
|
|
192
|
+
for (const match of String(text).matchAll(CSS_URL_RE)) {
|
|
193
|
+
const raw = match[2] !== undefined ? match[2] : match[3] !== undefined ? match[3] : match[4]
|
|
194
|
+
const reference = String(raw || '').trim()
|
|
195
|
+
if (reference && relativeReference(reference) && imageMimeOf(reference)) refs.add(reference)
|
|
196
|
+
}
|
|
197
|
+
if (!refs.size) return text
|
|
198
|
+
const resolved = new Map()
|
|
199
|
+
for (const reference of refs) {
|
|
200
|
+
const image = await loadImage(reference)
|
|
201
|
+
if (image) resolved.set(reference, image.url)
|
|
202
|
+
}
|
|
203
|
+
if (!resolved.size) return text
|
|
204
|
+
return String(text).replace(CSS_URL_RE, (match, quote, single, double, bare) => {
|
|
205
|
+
const url = resolved.get(String(single !== undefined ? single : double !== undefined ? double : bare).trim())
|
|
206
|
+
return url ? 'url(' + url + ')' : match
|
|
207
|
+
})
|
|
208
|
+
}
|
|
209
|
+
for (const element of template.content.querySelectorAll('style')) {
|
|
210
|
+
signal.throwIfAborted?.()
|
|
211
|
+
const next = await rewriteCssUrls(element.textContent)
|
|
212
|
+
if (next !== element.textContent) { element.textContent = next; mutated = true }
|
|
213
|
+
}
|
|
214
|
+
for (const element of template.content.querySelectorAll('[style]')) {
|
|
215
|
+
signal.throwIfAborted?.()
|
|
216
|
+
const next = await rewriteCssUrls(element.getAttribute('style'))
|
|
217
|
+
if (next !== element.getAttribute('style')) { element.setAttribute('style', next); mutated = true }
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// 图片已直接写入模板;仅在发生内联时回传序列化结果,
|
|
221
|
+
// 纯外链文档保持原始 srcDoc 上下文不变。
|
|
222
|
+
return { html: mutated ? template.innerHTML : source, assets }
|
|
104
223
|
}
|
|
105
224
|
|
|
106
225
|
/** Build the React component while retaining DSH runtime dependencies at the caller boundary. */
|
|
@@ -8,7 +8,6 @@
|
|
|
8
8
|
* 编辑动作)由 main 经工厂参数注入;纯辅助函数同时导出供 main 复用。
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import { hostBase } from './rpc.js'
|
|
12
11
|
import { CODE_LANG_BY_EXT } from './file-icons.js'
|
|
13
12
|
import { ICON_REFRESH, FILE_ICON_EDIT, FILE_ICON_PREVIEW } from './styles.js'
|
|
14
13
|
|
|
@@ -30,6 +29,21 @@ export function previewKind(path) {
|
|
|
30
29
|
return 'text'
|
|
31
30
|
}
|
|
32
31
|
|
|
32
|
+
/** 常见无扩展名/点文件本身就是文本配置,应允许直接编辑。 */
|
|
33
|
+
var EDITABLE_FILE_NAMES = new Set([
|
|
34
|
+
'.gitignore', '.gitattributes', '.gitmodules', '.dockerignore',
|
|
35
|
+
'.editorconfig', '.npmrc', '.yarnrc', '.yarnrc.yml',
|
|
36
|
+
'dockerfile', 'makefile', 'justfile', 'taskfile', 'procfile',
|
|
37
|
+
'vagrantfile', 'gemfile', 'rakefile', 'brewfile',
|
|
38
|
+
'license', 'licence', 'notice', 'authors', 'contributors',
|
|
39
|
+
])
|
|
40
|
+
|
|
41
|
+
function baseNameOf(path) {
|
|
42
|
+
var value = String(path || '').replace(/[\\/]+$/, '')
|
|
43
|
+
var slash = Math.max(value.lastIndexOf('/'), value.lastIndexOf('\\'))
|
|
44
|
+
return value.slice(slash + 1).toLowerCase()
|
|
45
|
+
}
|
|
46
|
+
|
|
33
47
|
/** RFC 4180 风格的分隔符解析(引号感知,"" 转义),返回行数组。 */
|
|
34
48
|
export function parseDelimited(text, delim) {
|
|
35
49
|
var rows = []
|
|
@@ -73,6 +87,7 @@ export function isEditableTextFile(path) {
|
|
|
73
87
|
var kind = previewKind(path)
|
|
74
88
|
var ext = extOfName(path)
|
|
75
89
|
return kind === 'markdown' || kind === 'html' || kind === 'json'
|
|
90
|
+
|| EDITABLE_FILE_NAMES.has(baseNameOf(path))
|
|
76
91
|
|| Object.prototype.hasOwnProperty.call(CODE_LANG_BY_EXT, ext)
|
|
77
92
|
}
|
|
78
93
|
|
|
@@ -264,13 +279,16 @@ export function createPreviewRenderers(ctx) {
|
|
|
264
279
|
var CSV_MAX_COLS = 64
|
|
265
280
|
|
|
266
281
|
// ------------------------------------------------------------------
|
|
267
|
-
// Markdown 相对图片 →
|
|
268
|
-
//
|
|
269
|
-
//
|
|
270
|
-
//
|
|
271
|
-
//
|
|
282
|
+
// Markdown 相对图片 → 官方 /api/file 文件路由词汇表(pathImages)。
|
|
283
|
+
// DSH 的 MarkdownText 渲染器只放行 http/https/mailto URL 与
|
|
284
|
+
// pathImages/fileImages 词汇表改写结果:桌面端 WebView 的页面基址是
|
|
285
|
+
// 自定义协议 dsh-app://app,把 src 改写成自家 img 端点会被
|
|
286
|
+
// sanitizeUrl 直接丢弃(alt 文本兜底);浏览器打开时才是 http。
|
|
287
|
+
// 因此改为向 MarkdownText 提供 pathImages 词汇表:相对路径以预览
|
|
288
|
+
// 文件所在目录为基解析成绝对路径,经官方 api/file 路由(浏览器为
|
|
289
|
+
// http 同源、桌面为 dsh-app://app/api/file,均在官方白名单内,
|
|
290
|
+
// 鉴权 cookie 由协议转发器注入)返回给 <img> 加载。
|
|
272
291
|
// ------------------------------------------------------------------
|
|
273
|
-
var MD_IMG_RE = /!\[([^\]]*)\]\(([^)\s]+)(?:\s+["'][^"']*["'])?\)/g
|
|
274
292
|
function posixResolve(dir, rel) {
|
|
275
293
|
var combined = (String(dir || '/').replace(/\/+$/, '') || '/') + '/' + String(rel)
|
|
276
294
|
var out = []
|
|
@@ -281,17 +299,24 @@ export function createPreviewRenderers(ctx) {
|
|
|
281
299
|
})
|
|
282
300
|
return '/' + out.join('/')
|
|
283
301
|
}
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
var
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
302
|
+
/** pathImages.resolve:本地路径 → 官方 api/file URL;非本地路径返回 undefined。 */
|
|
303
|
+
function localPathImageUrl(baseDir, value) {
|
|
304
|
+
var raw = String(value || '')
|
|
305
|
+
// 与 DSH localPathMediaUrl 同规:剥离 ?# 后缀,URL 转义解码一次。
|
|
306
|
+
var bare = raw.split(/[?#]/)[0]
|
|
307
|
+
var decoded
|
|
308
|
+
try { decoded = decodeURIComponent(bare) } catch (e) { return undefined }
|
|
309
|
+
if (!decoded || /[\u0000-\u001f\u007f]/.test(decoded)) return undefined
|
|
310
|
+
// 协议 URL(http/https/data 等)与 UNC 路径不属于本地路径词汇表,
|
|
311
|
+
// 留给官方 imageSource 处理;Windows 盘符路径按官方例外放行。
|
|
312
|
+
if (/^[a-z][a-z\d+.-]*:/i.test(decoded) && !/^[a-z]:[\\/]/i.test(decoded)) return undefined
|
|
313
|
+
if (/^[/\\]{2}/.test(decoded)) return undefined
|
|
314
|
+
var abs = decoded.charAt(0) === '/' || /^[a-z]:[\\/]/i.test(decoded)
|
|
315
|
+
? decoded
|
|
316
|
+
: posixResolve(baseDir || '/', decoded)
|
|
317
|
+
try {
|
|
318
|
+
return new URL('api/file?path=' + encodeURIComponent(abs), document.baseURI).href
|
|
319
|
+
} catch (e) { return undefined }
|
|
295
320
|
}
|
|
296
321
|
|
|
297
322
|
var MarkdownRenderBoundary = React.Component
|
|
@@ -403,7 +428,11 @@ export function createPreviewRenderers(ctx) {
|
|
|
403
428
|
if (MarkdownText) {
|
|
404
429
|
// Keep the original MarkdownText source contract visible for host integrations.
|
|
405
430
|
// create(MarkdownText, { text: md })
|
|
406
|
-
|
|
431
|
+
// 相对图片经 pathImages 词汇表交给官方 api/file 路由(见 localPathImageUrl 注释)。
|
|
432
|
+
var pathImages = props.baseDir
|
|
433
|
+
? { resolve: function (value) { return localPathImageUrl(props.baseDir, value) } }
|
|
434
|
+
: undefined
|
|
435
|
+
var md = text
|
|
407
436
|
var lines = String(md || '').split(/\r?\n/)
|
|
408
437
|
var chunks = []
|
|
409
438
|
var normal = []
|
|
@@ -411,7 +440,7 @@ export function createPreviewRenderers(ctx) {
|
|
|
411
440
|
function pushNormal() {
|
|
412
441
|
if (!normal.length) return
|
|
413
442
|
var value = normal.join('\n')
|
|
414
|
-
if (value.trim() !== '') chunks.push(create(MarkdownRenderBoundary, { key: 'md-' + chunks.length, text: value }, create(MarkdownText, { text: value })))
|
|
443
|
+
if (value.trim() !== '') chunks.push(create(MarkdownRenderBoundary, { key: 'md-' + chunks.length, text: value }, create(MarkdownText, { text: value, pathImages: pathImages })))
|
|
415
444
|
normal = []
|
|
416
445
|
}
|
|
417
446
|
function pushTable(headerLine) {
|
|
@@ -421,7 +450,7 @@ export function createPreviewRenderers(ctx) {
|
|
|
421
450
|
}
|
|
422
451
|
var headers = splitRow(headerLine)
|
|
423
452
|
var renderCell = function (cell, key) {
|
|
424
|
-
return create(MarkdownRenderBoundary, { key: key, text: cell }, create(MarkdownText, { text: cell }))
|
|
453
|
+
return create(MarkdownRenderBoundary, { key: key, text: cell }, create(MarkdownText, { text: cell, pathImages: pathImages }))
|
|
425
454
|
}
|
|
426
455
|
var head = create('tr', null, headers.map(function (cell, hi) {
|
|
427
456
|
return create('th', { key: hi, className: 'dfv-th' }, renderCell(cell, 'th-' + hi))
|
|
@@ -808,7 +837,7 @@ export function createPreviewRenderers(ctx) {
|
|
|
808
837
|
return {
|
|
809
838
|
codeMirrorLanguage: codeMirrorLanguage,
|
|
810
839
|
jsonColorNodes: jsonColorNodes,
|
|
811
|
-
|
|
840
|
+
localPathImageUrl: localPathImageUrl,
|
|
812
841
|
TextEditor: TextEditor,
|
|
813
842
|
MarkdownPreview: MarkdownPreview,
|
|
814
843
|
PdfPreview: PdfPreview,
|
package/lib/client.entry.js
CHANGED
package/lib/client.js
CHANGED
|
@@ -30734,6 +30734,18 @@
|
|
|
30734
30734
|
var ASSET_MAX_BYTES = 4 * 1024 * 1024;
|
|
30735
30735
|
var TOTAL_MAX_BYTES = 32 * 1024 * 1024;
|
|
30736
30736
|
var MAX_ASSETS = 64;
|
|
30737
|
+
var IMAGE_MAX_COUNT = 64;
|
|
30738
|
+
var IMAGE_DATA_MIME = {
|
|
30739
|
+
png: "image/png",
|
|
30740
|
+
jpg: "image/jpeg",
|
|
30741
|
+
jpeg: "image/jpeg",
|
|
30742
|
+
gif: "image/gif",
|
|
30743
|
+
webp: "image/webp",
|
|
30744
|
+
svg: "image/svg+xml",
|
|
30745
|
+
bmp: "image/bmp",
|
|
30746
|
+
ico: "image/x-icon"
|
|
30747
|
+
};
|
|
30748
|
+
var CSS_URL_RE = /url\(\s*('([^']*)'|"([^"]*)"|([^)'"]*))\s*\)/gi;
|
|
30737
30749
|
function base64ToText(data2) {
|
|
30738
30750
|
const raw = atob(data2);
|
|
30739
30751
|
const bytes = new Uint8Array(raw.length);
|
|
@@ -30747,6 +30759,19 @@
|
|
|
30747
30759
|
const cut = String(reference || "").search(/[?#]/);
|
|
30748
30760
|
return cut === -1 ? String(reference || "") : String(reference || "").slice(0, cut);
|
|
30749
30761
|
}
|
|
30762
|
+
function imageMimeOf(reference) {
|
|
30763
|
+
const match = /\.([A-Za-z0-9]+)$/.exec(assetPath(reference));
|
|
30764
|
+
return match ? IMAGE_DATA_MIME[match[1].toLowerCase()] : void 0;
|
|
30765
|
+
}
|
|
30766
|
+
function relativeKey(reference) {
|
|
30767
|
+
const out = [];
|
|
30768
|
+
for (const segment of String(reference || "").split("/")) {
|
|
30769
|
+
if (segment === "" || segment === ".") continue;
|
|
30770
|
+
if (segment === "..") out.pop();
|
|
30771
|
+
else out.push(segment);
|
|
30772
|
+
}
|
|
30773
|
+
return out.join("/");
|
|
30774
|
+
}
|
|
30750
30775
|
function prepareHtmlText(text) {
|
|
30751
30776
|
let next = String(text || "");
|
|
30752
30777
|
if (next.includes("js_content") || next.includes("data-src=")) {
|
|
@@ -30814,7 +30839,100 @@
|
|
|
30814
30839
|
} catch {
|
|
30815
30840
|
}
|
|
30816
30841
|
}
|
|
30817
|
-
|
|
30842
|
+
const imageCache = /* @__PURE__ */ new Map();
|
|
30843
|
+
function loadImage(reference) {
|
|
30844
|
+
let bare = assetPath(reference);
|
|
30845
|
+
try {
|
|
30846
|
+
bare = decodeURIComponent(bare);
|
|
30847
|
+
} catch {
|
|
30848
|
+
}
|
|
30849
|
+
if (!imageMimeOf(bare)) return Promise.resolve(null);
|
|
30850
|
+
const key = relativeKey(bare);
|
|
30851
|
+
let entry = imageCache.get(key);
|
|
30852
|
+
if (!entry) {
|
|
30853
|
+
if (imageCache.size >= IMAGE_MAX_COUNT) return Promise.resolve(null);
|
|
30854
|
+
entry = Promise.resolve(rpc("read-related", { path: rootPath, relativePath: bare, sessionId })).then((response) => {
|
|
30855
|
+
if (!response?.ok || response.size > ASSET_MAX_BYTES) return null;
|
|
30856
|
+
const bytes = Math.ceil(response.data.length * 3 / 4);
|
|
30857
|
+
if (total + bytes > TOTAL_MAX_BYTES) return null;
|
|
30858
|
+
total += bytes;
|
|
30859
|
+
return { url: "data:" + imageMimeOf(bare) + ";base64," + response.data, bytes };
|
|
30860
|
+
}).catch(() => null);
|
|
30861
|
+
imageCache.set(key, entry);
|
|
30862
|
+
}
|
|
30863
|
+
return entry;
|
|
30864
|
+
}
|
|
30865
|
+
let mutated = false;
|
|
30866
|
+
await Promise.all(
|
|
30867
|
+
Array.from(template.content.querySelectorAll('img[src],source[src],video[poster],input[type="image"][src]')).map(async (element) => {
|
|
30868
|
+
const attribute = element.localName === "video" ? "poster" : "src";
|
|
30869
|
+
const reference = element.getAttribute(attribute) || "";
|
|
30870
|
+
if (!relativeReference(reference) || !imageMimeOf(reference)) return;
|
|
30871
|
+
signal.throwIfAborted?.();
|
|
30872
|
+
const image = await loadImage(reference);
|
|
30873
|
+
if (image) {
|
|
30874
|
+
element.setAttribute(attribute, image.url);
|
|
30875
|
+
mutated = true;
|
|
30876
|
+
}
|
|
30877
|
+
})
|
|
30878
|
+
);
|
|
30879
|
+
for (const element of template.content.querySelectorAll("img[srcset],source[srcset]")) {
|
|
30880
|
+
signal.throwIfAborted?.();
|
|
30881
|
+
const value = element.getAttribute("srcset") || "";
|
|
30882
|
+
if (!value) continue;
|
|
30883
|
+
const parts = await Promise.all(value.split(",").map(async (part) => {
|
|
30884
|
+
const trimmed = part.trim();
|
|
30885
|
+
if (!trimmed) return part;
|
|
30886
|
+
const cut = trimmed.search(/\s/);
|
|
30887
|
+
const url = cut === -1 ? trimmed : trimmed.slice(0, cut);
|
|
30888
|
+
const rest = cut === -1 ? "" : trimmed.slice(cut);
|
|
30889
|
+
if (!relativeReference(url)) return part;
|
|
30890
|
+
const image = await loadImage(url);
|
|
30891
|
+
return image ? image.url + rest : part;
|
|
30892
|
+
}));
|
|
30893
|
+
const next = parts.join(",");
|
|
30894
|
+
if (next !== value) {
|
|
30895
|
+
element.setAttribute("srcset", next);
|
|
30896
|
+
mutated = true;
|
|
30897
|
+
}
|
|
30898
|
+
}
|
|
30899
|
+
async function rewriteCssUrls(text2) {
|
|
30900
|
+
if (!text2 || String(text2).indexOf("url(") === -1) return text2;
|
|
30901
|
+
const refs = /* @__PURE__ */ new Set();
|
|
30902
|
+
for (const match of String(text2).matchAll(CSS_URL_RE)) {
|
|
30903
|
+
const raw = match[2] !== void 0 ? match[2] : match[3] !== void 0 ? match[3] : match[4];
|
|
30904
|
+
const reference = String(raw || "").trim();
|
|
30905
|
+
if (reference && relativeReference(reference) && imageMimeOf(reference)) refs.add(reference);
|
|
30906
|
+
}
|
|
30907
|
+
if (!refs.size) return text2;
|
|
30908
|
+
const resolved = /* @__PURE__ */ new Map();
|
|
30909
|
+
for (const reference of refs) {
|
|
30910
|
+
const image = await loadImage(reference);
|
|
30911
|
+
if (image) resolved.set(reference, image.url);
|
|
30912
|
+
}
|
|
30913
|
+
if (!resolved.size) return text2;
|
|
30914
|
+
return String(text2).replace(CSS_URL_RE, (match, quote, single, double, bare) => {
|
|
30915
|
+
const url = resolved.get(String(single !== void 0 ? single : double !== void 0 ? double : bare).trim());
|
|
30916
|
+
return url ? "url(" + url + ")" : match;
|
|
30917
|
+
});
|
|
30918
|
+
}
|
|
30919
|
+
for (const element of template.content.querySelectorAll("style")) {
|
|
30920
|
+
signal.throwIfAborted?.();
|
|
30921
|
+
const next = await rewriteCssUrls(element.textContent);
|
|
30922
|
+
if (next !== element.textContent) {
|
|
30923
|
+
element.textContent = next;
|
|
30924
|
+
mutated = true;
|
|
30925
|
+
}
|
|
30926
|
+
}
|
|
30927
|
+
for (const element of template.content.querySelectorAll("[style]")) {
|
|
30928
|
+
signal.throwIfAborted?.();
|
|
30929
|
+
const next = await rewriteCssUrls(element.getAttribute("style"));
|
|
30930
|
+
if (next !== element.getAttribute("style")) {
|
|
30931
|
+
element.setAttribute("style", next);
|
|
30932
|
+
mutated = true;
|
|
30933
|
+
}
|
|
30934
|
+
}
|
|
30935
|
+
return { html: mutated ? template.innerHTML : source, assets };
|
|
30818
30936
|
}
|
|
30819
30937
|
function createHtmlPreview({ React, create, rpc, translate }) {
|
|
30820
30938
|
return function HtmlPreview(props) {
|
|
@@ -30845,6 +30963,110 @@
|
|
|
30845
30963
|
window.__DSH_SOUP_HTML_PREVIEW__ = { createHtmlPreview };
|
|
30846
30964
|
}
|
|
30847
30965
|
|
|
30966
|
+
// lib/client/file-address.js
|
|
30967
|
+
var FILE_ADDRESS_PREFIX = "dsh-resource://file/";
|
|
30968
|
+
function decodeSegmentSafe(segment) {
|
|
30969
|
+
try {
|
|
30970
|
+
return decodeURIComponent(segment);
|
|
30971
|
+
} catch (e) {
|
|
30972
|
+
return null;
|
|
30973
|
+
}
|
|
30974
|
+
}
|
|
30975
|
+
function parseFileAddress(address) {
|
|
30976
|
+
if (typeof address !== "string" || address.indexOf(FILE_ADDRESS_PREFIX) !== 0) return null;
|
|
30977
|
+
var end = address.search(/[?#]/);
|
|
30978
|
+
var body = address.slice(FILE_ADDRESS_PREFIX.length, end === -1 ? void 0 : end);
|
|
30979
|
+
var parts = body.split("/");
|
|
30980
|
+
if (parts[0] === "session") {
|
|
30981
|
+
var sessionId = decodeSegmentSafe(parts[1] || "");
|
|
30982
|
+
if (!sessionId) return null;
|
|
30983
|
+
var segments = [];
|
|
30984
|
+
for (var i = 2; i < parts.length; i++) {
|
|
30985
|
+
var seg = decodeSegmentSafe(parts[i]);
|
|
30986
|
+
if (seg === null) return null;
|
|
30987
|
+
segments.push(seg);
|
|
30988
|
+
}
|
|
30989
|
+
if (!segments.length) return null;
|
|
30990
|
+
return { scope: "session", sessionId, path: segments.join("/") };
|
|
30991
|
+
}
|
|
30992
|
+
if (parts[0] === "absolute") {
|
|
30993
|
+
var absolute = [];
|
|
30994
|
+
for (var j = 1; j < parts.length; j++) {
|
|
30995
|
+
var abs = decodeSegmentSafe(parts[j]);
|
|
30996
|
+
if (abs === null) return null;
|
|
30997
|
+
absolute.push(abs);
|
|
30998
|
+
}
|
|
30999
|
+
if (!absolute.length) return null;
|
|
31000
|
+
return { scope: "absolute", path: "/" + absolute.join("/") };
|
|
31001
|
+
}
|
|
31002
|
+
return null;
|
|
31003
|
+
}
|
|
31004
|
+
function fileAddressMatches(address, sessionIdHint, activeSessionId) {
|
|
31005
|
+
var parsed = parseFileAddress(address);
|
|
31006
|
+
if (!parsed) return false;
|
|
31007
|
+
if (parsed.scope === "session" && (sessionIdHint || parsed.sessionId) !== activeSessionId) return false;
|
|
31008
|
+
return true;
|
|
31009
|
+
}
|
|
31010
|
+
|
|
31011
|
+
// lib/client/file-address-bridge.js
|
|
31012
|
+
function bridgeFileAddressOpener(service, ctx) {
|
|
31013
|
+
if (!service || typeof service.openResource !== "function") return null;
|
|
31014
|
+
if (service.__dshSoupFileBridge) return null;
|
|
31015
|
+
var originalOpenResource = service.openResource;
|
|
31016
|
+
var originalOpenResourceIn = service.openResourceIn;
|
|
31017
|
+
var openFileAddressInSoup = ctx.openFileAddressInSoup;
|
|
31018
|
+
var getActiveSessionId = ctx.getActiveSessionId;
|
|
31019
|
+
function openInSoup(address, sessionIdHint) {
|
|
31020
|
+
if (!fileAddressMatches(address, sessionIdHint, getActiveSessionId())) return false;
|
|
31021
|
+
try {
|
|
31022
|
+
var task = openFileAddressInSoup(address, sessionIdHint);
|
|
31023
|
+
if (task && typeof task.catch === "function") task.catch(function() {
|
|
31024
|
+
});
|
|
31025
|
+
return true;
|
|
31026
|
+
} catch (err) {
|
|
31027
|
+
return false;
|
|
31028
|
+
}
|
|
31029
|
+
}
|
|
31030
|
+
var bridgedOpenResource = function(address, options) {
|
|
31031
|
+
if (openInSoup(address, null)) return void 0;
|
|
31032
|
+
return originalOpenResource.call(this, address, options);
|
|
31033
|
+
};
|
|
31034
|
+
var bridgedOpenResourceIn = typeof originalOpenResourceIn === "function" ? function(sessionId, address, options) {
|
|
31035
|
+
if (openInSoup(address, sessionId)) return void 0;
|
|
31036
|
+
return originalOpenResourceIn.call(this, sessionId, address, options);
|
|
31037
|
+
} : null;
|
|
31038
|
+
try {
|
|
31039
|
+
service.openResource = bridgedOpenResource;
|
|
31040
|
+
if (bridgedOpenResourceIn) service.openResourceIn = bridgedOpenResourceIn;
|
|
31041
|
+
if (service.openResource !== bridgedOpenResource || bridgedOpenResourceIn && service.openResourceIn !== bridgedOpenResourceIn) return null;
|
|
31042
|
+
try {
|
|
31043
|
+
Object.defineProperty(service, "__dshSoupFileBridge", { value: true, configurable: true });
|
|
31044
|
+
} catch (err) {
|
|
31045
|
+
service.__dshSoupFileBridge = true;
|
|
31046
|
+
}
|
|
31047
|
+
} catch (err) {
|
|
31048
|
+
try {
|
|
31049
|
+
if (service.openResource === bridgedOpenResource) service.openResource = originalOpenResource;
|
|
31050
|
+
if (bridgedOpenResourceIn && service.openResourceIn === bridgedOpenResourceIn) {
|
|
31051
|
+
service.openResourceIn = originalOpenResourceIn;
|
|
31052
|
+
}
|
|
31053
|
+
} catch (restoreErr) {
|
|
31054
|
+
}
|
|
31055
|
+
return null;
|
|
31056
|
+
}
|
|
31057
|
+
return function() {
|
|
31058
|
+
if (service.openResource === bridgedOpenResource) service.openResource = originalOpenResource;
|
|
31059
|
+
if (bridgedOpenResourceIn && service.openResourceIn === bridgedOpenResourceIn) {
|
|
31060
|
+
service.openResourceIn = originalOpenResourceIn;
|
|
31061
|
+
}
|
|
31062
|
+
try {
|
|
31063
|
+
delete service.__dshSoupFileBridge;
|
|
31064
|
+
} catch (err) {
|
|
31065
|
+
}
|
|
31066
|
+
};
|
|
31067
|
+
}
|
|
31068
|
+
if (typeof window !== "undefined") window.__DSH_SOUP_FILE_ADDRESS_BRIDGE__ = { bridgeFileAddressOpener };
|
|
31069
|
+
|
|
30848
31070
|
// lib/client/styles.js
|
|
30849
31071
|
var ICON_REFRESH = '<svg xmlns="http://www.w3.org/2000/svg" width="16" viewBox="0 0 18 18"><path fill="currentColor" d="M9 13.5c-2.49 0-4.5-2.01-4.5-4.5S6.51 4.5 9 4.5c1.24 0 2.36.52 3.17 1.33L10 8h5V3l-1.76 1.76A6 6 0 0 0 9 3C5.69 3 3.01 5.69 3.01 9S5.69 15 9 15a5.98 5.98 0 0 0 5.9-5h-1.52c-.46 2-2.24 3.5-4.38 3.5"/></svg>';
|
|
30850
31072
|
var ICON_NEW_FOLDER = '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24"><path fill="currentColor" d="M20 6h-8l-2-2H4c-1.1 0-1.99.9-1.99 2L2 18c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2zm-2 7h-3v3h-2v-3h-3v-2h3V8h2v3h3v2z"/></svg>';
|
|
@@ -31370,51 +31592,6 @@
|
|
|
31370
31592
|
}
|
|
31371
31593
|
var setiTag;
|
|
31372
31594
|
|
|
31373
|
-
// lib/client/file-address.js
|
|
31374
|
-
var FILE_ADDRESS_PREFIX = "dsh-resource://file/";
|
|
31375
|
-
function decodeSegmentSafe(segment) {
|
|
31376
|
-
try {
|
|
31377
|
-
return decodeURIComponent(segment);
|
|
31378
|
-
} catch (e) {
|
|
31379
|
-
return null;
|
|
31380
|
-
}
|
|
31381
|
-
}
|
|
31382
|
-
function parseFileAddress(address) {
|
|
31383
|
-
if (typeof address !== "string" || address.indexOf(FILE_ADDRESS_PREFIX) !== 0) return null;
|
|
31384
|
-
var end = address.search(/[?#]/);
|
|
31385
|
-
var body = address.slice(FILE_ADDRESS_PREFIX.length, end === -1 ? void 0 : end);
|
|
31386
|
-
var parts = body.split("/");
|
|
31387
|
-
if (parts[0] === "session") {
|
|
31388
|
-
var sessionId = decodeSegmentSafe(parts[1] || "");
|
|
31389
|
-
if (!sessionId) return null;
|
|
31390
|
-
var segments = [];
|
|
31391
|
-
for (var i = 2; i < parts.length; i++) {
|
|
31392
|
-
var seg = decodeSegmentSafe(parts[i]);
|
|
31393
|
-
if (seg === null) return null;
|
|
31394
|
-
segments.push(seg);
|
|
31395
|
-
}
|
|
31396
|
-
if (!segments.length) return null;
|
|
31397
|
-
return { scope: "session", sessionId, path: segments.join("/") };
|
|
31398
|
-
}
|
|
31399
|
-
if (parts[0] === "absolute") {
|
|
31400
|
-
var absolute = [];
|
|
31401
|
-
for (var j = 1; j < parts.length; j++) {
|
|
31402
|
-
var abs = decodeSegmentSafe(parts[j]);
|
|
31403
|
-
if (abs === null) return null;
|
|
31404
|
-
absolute.push(abs);
|
|
31405
|
-
}
|
|
31406
|
-
if (!absolute.length) return null;
|
|
31407
|
-
return { scope: "absolute", path: "/" + absolute.join("/") };
|
|
31408
|
-
}
|
|
31409
|
-
return null;
|
|
31410
|
-
}
|
|
31411
|
-
function fileAddressMatches(address, sessionIdHint, activeSessionId) {
|
|
31412
|
-
var parsed = parseFileAddress(address);
|
|
31413
|
-
if (!parsed) return false;
|
|
31414
|
-
if (parsed.scope === "session" && (sessionIdHint || parsed.sessionId) !== activeSessionId) return false;
|
|
31415
|
-
return true;
|
|
31416
|
-
}
|
|
31417
|
-
|
|
31418
31595
|
// lib/client/preview-renderers.js
|
|
31419
31596
|
function extOfName2(p) {
|
|
31420
31597
|
var m = /\.([A-Za-z0-9]+)$/.exec(String(p || ""));
|
|
@@ -31430,6 +31607,35 @@
|
|
|
31430
31607
|
if (ext === "tsv") return "tsv";
|
|
31431
31608
|
return "text";
|
|
31432
31609
|
}
|
|
31610
|
+
var EDITABLE_FILE_NAMES = /* @__PURE__ */ new Set([
|
|
31611
|
+
".gitignore",
|
|
31612
|
+
".gitattributes",
|
|
31613
|
+
".gitmodules",
|
|
31614
|
+
".dockerignore",
|
|
31615
|
+
".editorconfig",
|
|
31616
|
+
".npmrc",
|
|
31617
|
+
".yarnrc",
|
|
31618
|
+
".yarnrc.yml",
|
|
31619
|
+
"dockerfile",
|
|
31620
|
+
"makefile",
|
|
31621
|
+
"justfile",
|
|
31622
|
+
"taskfile",
|
|
31623
|
+
"procfile",
|
|
31624
|
+
"vagrantfile",
|
|
31625
|
+
"gemfile",
|
|
31626
|
+
"rakefile",
|
|
31627
|
+
"brewfile",
|
|
31628
|
+
"license",
|
|
31629
|
+
"licence",
|
|
31630
|
+
"notice",
|
|
31631
|
+
"authors",
|
|
31632
|
+
"contributors"
|
|
31633
|
+
]);
|
|
31634
|
+
function baseNameOf(path) {
|
|
31635
|
+
var value = String(path || "").replace(/[\\/]+$/, "");
|
|
31636
|
+
var slash3 = Math.max(value.lastIndexOf("/"), value.lastIndexOf("\\"));
|
|
31637
|
+
return value.slice(slash3 + 1).toLowerCase();
|
|
31638
|
+
}
|
|
31433
31639
|
function parseDelimited(text, delim) {
|
|
31434
31640
|
var rows = [];
|
|
31435
31641
|
var row = [];
|
|
@@ -31479,7 +31685,7 @@
|
|
|
31479
31685
|
function isEditableTextFile(path) {
|
|
31480
31686
|
var kind = previewKind(path);
|
|
31481
31687
|
var ext = extOfName2(path);
|
|
31482
|
-
return kind === "markdown" || kind === "html" || kind === "json" || Object.prototype.hasOwnProperty.call(CODE_LANG_BY_EXT, ext);
|
|
31688
|
+
return kind === "markdown" || kind === "html" || kind === "json" || EDITABLE_FILE_NAMES.has(baseNameOf(path)) || Object.prototype.hasOwnProperty.call(CODE_LANG_BY_EXT, ext);
|
|
31483
31689
|
}
|
|
31484
31690
|
function createPreviewRenderers(ctx) {
|
|
31485
31691
|
var React = ctx.React;
|
|
@@ -31639,7 +31845,6 @@
|
|
|
31639
31845
|
}
|
|
31640
31846
|
var CSV_MAX_ROWS = 500;
|
|
31641
31847
|
var CSV_MAX_COLS = 64;
|
|
31642
|
-
var MD_IMG_RE = /!\[([^\]]*)\]\(([^)\s]+)(?:\s+["'][^"']*["'])?\)/g;
|
|
31643
31848
|
function posixResolve(dir, rel) {
|
|
31644
31849
|
var combined = (String(dir || "/").replace(/\/+$/, "") || "/") + "/" + String(rel);
|
|
31645
31850
|
var out = [];
|
|
@@ -31653,17 +31858,24 @@
|
|
|
31653
31858
|
});
|
|
31654
31859
|
return "/" + out.join("/");
|
|
31655
31860
|
}
|
|
31656
|
-
function
|
|
31657
|
-
|
|
31658
|
-
|
|
31659
|
-
|
|
31660
|
-
|
|
31661
|
-
|
|
31662
|
-
|
|
31663
|
-
|
|
31664
|
-
|
|
31665
|
-
|
|
31666
|
-
|
|
31861
|
+
function localPathImageUrl(baseDir, value) {
|
|
31862
|
+
var raw = String(value || "");
|
|
31863
|
+
var bare = raw.split(/[?#]/)[0];
|
|
31864
|
+
var decoded;
|
|
31865
|
+
try {
|
|
31866
|
+
decoded = decodeURIComponent(bare);
|
|
31867
|
+
} catch (e) {
|
|
31868
|
+
return void 0;
|
|
31869
|
+
}
|
|
31870
|
+
if (!decoded || /[\u0000-\u001f\u007f]/.test(decoded)) return void 0;
|
|
31871
|
+
if (/^[a-z][a-z\d+.-]*:/i.test(decoded) && !/^[a-z]:[\\/]/i.test(decoded)) return void 0;
|
|
31872
|
+
if (/^[/\\]{2}/.test(decoded)) return void 0;
|
|
31873
|
+
var abs = decoded.charAt(0) === "/" || /^[a-z]:[\\/]/i.test(decoded) ? decoded : posixResolve(baseDir || "/", decoded);
|
|
31874
|
+
try {
|
|
31875
|
+
return new URL("api/file?path=" + encodeURIComponent(abs), document.baseURI).href;
|
|
31876
|
+
} catch (e) {
|
|
31877
|
+
return void 0;
|
|
31878
|
+
}
|
|
31667
31879
|
}
|
|
31668
31880
|
var MarkdownRenderBoundary = React.Component ? class MarkdownRenderBoundary extends React.Component {
|
|
31669
31881
|
constructor(props) {
|
|
@@ -31785,7 +31997,7 @@
|
|
|
31785
31997
|
let pushNormal = function() {
|
|
31786
31998
|
if (!normal.length) return;
|
|
31787
31999
|
var value = normal.join("\n");
|
|
31788
|
-
if (value.trim() !== "") chunks.push(create(MarkdownRenderBoundary, { key: "md-" + chunks.length, text: value }, create(MarkdownText, { text: value })));
|
|
32000
|
+
if (value.trim() !== "") chunks.push(create(MarkdownRenderBoundary, { key: "md-" + chunks.length, text: value }, create(MarkdownText, { text: value, pathImages })));
|
|
31789
32001
|
normal = [];
|
|
31790
32002
|
}, pushTable = function(headerLine) {
|
|
31791
32003
|
var splitRow = function(value) {
|
|
@@ -31796,7 +32008,7 @@
|
|
|
31796
32008
|
};
|
|
31797
32009
|
var headers = splitRow(headerLine);
|
|
31798
32010
|
var renderCell = function(cell, key) {
|
|
31799
|
-
return create(MarkdownRenderBoundary, { key, text: cell }, create(MarkdownText, { text: cell }));
|
|
32011
|
+
return create(MarkdownRenderBoundary, { key, text: cell }, create(MarkdownText, { text: cell, pathImages }));
|
|
31800
32012
|
};
|
|
31801
32013
|
var head = create("tr", null, headers.map(function(cell, hi) {
|
|
31802
32014
|
return create("th", { key: hi, className: "dfv-th" }, renderCell(cell, "th-" + hi));
|
|
@@ -31824,7 +32036,10 @@
|
|
|
31824
32036
|
)
|
|
31825
32037
|
));
|
|
31826
32038
|
};
|
|
31827
|
-
var
|
|
32039
|
+
var pathImages = props.baseDir ? { resolve: function(value) {
|
|
32040
|
+
return localPathImageUrl(props.baseDir, value);
|
|
32041
|
+
} } : void 0;
|
|
32042
|
+
var md = text;
|
|
31828
32043
|
var lines = String(md || "").split(/\r?\n/);
|
|
31829
32044
|
var chunks = [];
|
|
31830
32045
|
var normal = [];
|
|
@@ -32234,7 +32449,7 @@
|
|
|
32234
32449
|
return {
|
|
32235
32450
|
codeMirrorLanguage,
|
|
32236
32451
|
jsonColorNodes,
|
|
32237
|
-
|
|
32452
|
+
localPathImageUrl,
|
|
32238
32453
|
TextEditor,
|
|
32239
32454
|
MarkdownPreview,
|
|
32240
32455
|
PdfPreview,
|
|
@@ -33807,7 +34022,7 @@
|
|
|
33807
34022
|
var uploadTargetDir = null;
|
|
33808
34023
|
var DETAILS_MIN = 300;
|
|
33809
34024
|
var DETAILS_MAX = 520;
|
|
33810
|
-
var DETAILS_DEFAULT =
|
|
34025
|
+
var DETAILS_DEFAULT = 300;
|
|
33811
34026
|
function clampW(w) {
|
|
33812
34027
|
return Math.max(DETAILS_MIN, Math.min(DETAILS_MAX, w));
|
|
33813
34028
|
}
|
|
@@ -34853,7 +35068,8 @@
|
|
|
34853
35068
|
document.removeEventListener("click", onOfficialExpand, true);
|
|
34854
35069
|
});
|
|
34855
35070
|
}
|
|
34856
|
-
var
|
|
35071
|
+
var bridgeDisposers = [];
|
|
35072
|
+
var bridgedServices = typeof WeakSet === "function" ? /* @__PURE__ */ new WeakSet() : [];
|
|
34857
35073
|
function resolveSidebarRight() {
|
|
34858
35074
|
try {
|
|
34859
35075
|
if (ctx.sidebarRight) return ctx.sidebarRight;
|
|
@@ -34875,42 +35091,43 @@
|
|
|
34875
35091
|
}
|
|
34876
35092
|
return null;
|
|
34877
35093
|
}
|
|
34878
|
-
function
|
|
34879
|
-
|
|
34880
|
-
|
|
34881
|
-
|
|
34882
|
-
if (
|
|
34883
|
-
|
|
34884
|
-
|
|
34885
|
-
|
|
34886
|
-
|
|
34887
|
-
|
|
34888
|
-
|
|
34889
|
-
|
|
34890
|
-
|
|
34891
|
-
};
|
|
34892
|
-
if (typeof origResourceIn === "function") {
|
|
34893
|
-
service.openResourceIn = function(sessionId, address, options) {
|
|
34894
|
-
if (fileAddressMatches(address, sessionId, activeSessionId)) {
|
|
34895
|
-
openFileAddressInSoup(address, sessionId);
|
|
34896
|
-
return;
|
|
34897
|
-
}
|
|
34898
|
-
return origResourceIn.call(service, sessionId, address, options);
|
|
34899
|
-
};
|
|
34900
|
-
}
|
|
34901
|
-
try {
|
|
34902
|
-
Object.defineProperty(service, "__dshSoupFileBridge", { value: true });
|
|
34903
|
-
} catch (e) {
|
|
34904
|
-
service.__dshSoupFileBridge = true;
|
|
34905
|
-
}
|
|
35094
|
+
function hasBridgedService(service) {
|
|
35095
|
+
return bridgedServices instanceof WeakSet ? bridgedServices.has(service) : bridgedServices.indexOf(service) !== -1;
|
|
35096
|
+
}
|
|
35097
|
+
function rememberBridgedService(service) {
|
|
35098
|
+
if (bridgedServices instanceof WeakSet) bridgedServices.add(service);
|
|
35099
|
+
else bridgedServices.push(service);
|
|
35100
|
+
}
|
|
35101
|
+
function patchSidebarRightBridge(service) {
|
|
35102
|
+
service = service || resolveSidebarRight();
|
|
35103
|
+
if (!service || hasBridgedService(service)) return !!service;
|
|
35104
|
+
if (service.__dshSoupFileBridge) {
|
|
35105
|
+
rememberBridgedService(service);
|
|
35106
|
+
return true;
|
|
34906
35107
|
}
|
|
34907
|
-
|
|
35108
|
+
var disposeBridge = bridgeFileAddressOpener(service, {
|
|
35109
|
+
openFileAddressInSoup,
|
|
35110
|
+
getActiveSessionId: function() {
|
|
35111
|
+
return activeSessionId;
|
|
35112
|
+
}
|
|
35113
|
+
});
|
|
35114
|
+
if (!disposeBridge) return false;
|
|
35115
|
+
rememberBridgedService(service);
|
|
35116
|
+
bridgeDisposers.push(disposeBridge);
|
|
34908
35117
|
try {
|
|
34909
35118
|
if (typeof window !== "undefined") window.__DSH_SOUP_FILE_BRIDGE = true;
|
|
34910
35119
|
} catch (e) {
|
|
34911
35120
|
}
|
|
34912
35121
|
return true;
|
|
34913
35122
|
}
|
|
35123
|
+
if (typeof ctx.inject === "function") {
|
|
35124
|
+
try {
|
|
35125
|
+
ctx.inject(["sidebarRight"], function(scope) {
|
|
35126
|
+
patchSidebarRightBridge(scope && scope.sidebarRight);
|
|
35127
|
+
});
|
|
35128
|
+
} catch (e) {
|
|
35129
|
+
}
|
|
35130
|
+
}
|
|
34914
35131
|
patchSidebarRightBridge();
|
|
34915
35132
|
var bridgeTries = 0;
|
|
34916
35133
|
var bridgeTimer = setInterval(function() {
|
|
@@ -34920,6 +35137,14 @@
|
|
|
34920
35137
|
disposers.push(function() {
|
|
34921
35138
|
clearInterval(bridgeTimer);
|
|
34922
35139
|
});
|
|
35140
|
+
disposers.push(function() {
|
|
35141
|
+
bridgeDisposers.forEach(function(disposeBridge) {
|
|
35142
|
+
try {
|
|
35143
|
+
disposeBridge();
|
|
35144
|
+
} catch (e) {
|
|
35145
|
+
}
|
|
35146
|
+
});
|
|
35147
|
+
});
|
|
34923
35148
|
if (ctx.locale && typeof ctx.locale.register === "function") {
|
|
34924
35149
|
ctx.effect(function() {
|
|
34925
35150
|
return ctx.locale.register(NS, DICT);
|
package/lib/client.main.js
CHANGED
|
@@ -3,7 +3,8 @@ import { NS, DICT } from './client/i18n.js'
|
|
|
3
3
|
import { iconSvgFor } from './client/file-icons.js'
|
|
4
4
|
import { createRpc, hostBase } from './client/rpc.js'
|
|
5
5
|
import { createHtmlPreview } from './client/html-preview.js'
|
|
6
|
-
import { parseFileAddress
|
|
6
|
+
import { parseFileAddress } from './client/file-address.js'
|
|
7
|
+
import { bridgeFileAddressOpener } from './client/file-address-bridge.js'
|
|
7
8
|
import { createPreviewRenderers, isEditableTextFile } from './client/preview-renderers.js'
|
|
8
9
|
import { createSpeedBadge } from './client/speed-badge.js'
|
|
9
10
|
import { createGoalBar } from './client/goal-bar.js'
|
|
@@ -17,7 +18,7 @@ import { createExplorerView } from './client/explorer-view.js'
|
|
|
17
18
|
* 布局:注册进 shell 原生的 `details` 右列(并列网格,非悬浮),
|
|
18
19
|
* 有 Session log 时在它右侧放 📁;空白新会话则在输入框上方、
|
|
19
20
|
* workspace/preset 控件行的右端提供同一入口。
|
|
20
|
-
* 宽度对齐宿主 details 合同:默认
|
|
21
|
+
* 宽度对齐宿主 details 合同:默认 300px、可拖 300–520px(直接改写 shell 网格
|
|
21
22
|
* 的 details 轨,并用 MutationObserver 在 shell 重渲染后维持宽度)。
|
|
22
23
|
*
|
|
23
24
|
* 与宿主通信:走同源 HTTP 路由 `/api/dsh-soup`(POST JSON),
|
|
@@ -385,10 +386,11 @@ window.__ModuleLoader__.load({
|
|
|
385
386
|
document.addEventListener('click', onOfficialExpand, true)
|
|
386
387
|
disposers.push(function () { document.removeEventListener('click', onOfficialExpand, true) })
|
|
387
388
|
}
|
|
388
|
-
//
|
|
389
|
-
//
|
|
390
|
-
//
|
|
391
|
-
var
|
|
389
|
+
// 官方产物链接桥接:产物卡片并不把 file 地址放进 DOM,而是直接调
|
|
390
|
+
// sidebarRight.openResource()。rightbar 槽位被替换后仍需接住该调用。
|
|
391
|
+
// 首选 ctx.inject 订阅服务就绪/重建;反射轮询仅保留给旧版宿主兼容。
|
|
392
|
+
var bridgeDisposers = []
|
|
393
|
+
var bridgedServices = typeof WeakSet === 'function' ? new WeakSet() : []
|
|
392
394
|
function resolveSidebarRight() {
|
|
393
395
|
try { if (ctx.sidebarRight) return ctx.sidebarRight } catch (e) {}
|
|
394
396
|
try {
|
|
@@ -405,30 +407,37 @@ window.__ModuleLoader__.load({
|
|
|
405
407
|
} catch (e) {}
|
|
406
408
|
return null
|
|
407
409
|
}
|
|
408
|
-
function
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
if (
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
if (fileAddressMatches(address, sessionId, activeSessionId)) { openFileAddressInSoup(address, sessionId); return }
|
|
422
|
-
return origResourceIn.call(service, sessionId, address, options)
|
|
423
|
-
}
|
|
424
|
-
}
|
|
425
|
-
try { Object.defineProperty(service, '__dshSoupFileBridge', { value: true }) }
|
|
426
|
-
catch (e) { service.__dshSoupFileBridge = true }
|
|
410
|
+
function hasBridgedService(service) {
|
|
411
|
+
return bridgedServices instanceof WeakSet ? bridgedServices.has(service) : bridgedServices.indexOf(service) !== -1
|
|
412
|
+
}
|
|
413
|
+
function rememberBridgedService(service) {
|
|
414
|
+
if (bridgedServices instanceof WeakSet) bridgedServices.add(service)
|
|
415
|
+
else bridgedServices.push(service)
|
|
416
|
+
}
|
|
417
|
+
function patchSidebarRightBridge(service) {
|
|
418
|
+
service = service || resolveSidebarRight()
|
|
419
|
+
if (!service || hasBridgedService(service)) return !!service
|
|
420
|
+
if (service.__dshSoupFileBridge) {
|
|
421
|
+
rememberBridgedService(service)
|
|
422
|
+
return true
|
|
427
423
|
}
|
|
428
|
-
|
|
424
|
+
var disposeBridge = bridgeFileAddressOpener(service, {
|
|
425
|
+
openFileAddressInSoup: openFileAddressInSoup,
|
|
426
|
+
getActiveSessionId: function () { return activeSessionId },
|
|
427
|
+
})
|
|
428
|
+
if (!disposeBridge) return false
|
|
429
|
+
rememberBridgedService(service)
|
|
430
|
+
bridgeDisposers.push(disposeBridge)
|
|
429
431
|
try { if (typeof window !== 'undefined') window.__DSH_SOUP_FILE_BRIDGE = true } catch (e) {}
|
|
430
432
|
return true
|
|
431
433
|
}
|
|
434
|
+
if (typeof ctx.inject === 'function') {
|
|
435
|
+
try {
|
|
436
|
+
ctx.inject(['sidebarRight'], function (scope) {
|
|
437
|
+
patchSidebarRightBridge(scope && scope.sidebarRight)
|
|
438
|
+
})
|
|
439
|
+
} catch (e) {}
|
|
440
|
+
}
|
|
432
441
|
patchSidebarRightBridge()
|
|
433
442
|
var bridgeTries = 0
|
|
434
443
|
var bridgeTimer = setInterval(function () {
|
|
@@ -436,6 +445,9 @@ window.__ModuleLoader__.load({
|
|
|
436
445
|
if (patchSidebarRightBridge() || bridgeTries >= 10) clearInterval(bridgeTimer)
|
|
437
446
|
}, 1000)
|
|
438
447
|
disposers.push(function () { clearInterval(bridgeTimer) })
|
|
448
|
+
disposers.push(function () {
|
|
449
|
+
bridgeDisposers.forEach(function (disposeBridge) { try { disposeBridge() } catch (e) {} })
|
|
450
|
+
})
|
|
439
451
|
// i18n:注册双语词典;模块级 T 供组件外回调(菜单/错误横幅)取当前语言文案。
|
|
440
452
|
if (ctx.locale && typeof ctx.locale.register === 'function') {
|
|
441
453
|
ctx.effect(function () { return ctx.locale.register(NS, DICT) }, 'dsh-soup: dictionaries')
|
package/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lyhue1991/dsh-soup",
|
|
3
3
|
"description": "dsh 体验增强四件套 = Jupyter风格SideBar + 中央文件预览Tab + 多行GoalBar + 实时Token流速徽标",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.7.1",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"scripts": {
|
|
8
8
|
"build": "esbuild lib/client.entry.js --bundle --format=iife --platform=browser --target=es2020 --outfile=lib/client.js",
|
|
9
9
|
"prepublishOnly": "npm run build",
|
|
10
|
-
"test": "npm run build && node --test test/client-goal.spec.mjs test/host.spec.mjs"
|
|
10
|
+
"test": "npm run build && node --test test/client-goal.spec.mjs test/file-address-bridge.spec.mjs test/html-preview.spec.mjs test/host.spec.mjs"
|
|
11
11
|
},
|
|
12
12
|
"exports": {
|
|
13
13
|
".": "./lib/index.js",
|
|
@@ -31,7 +31,8 @@
|
|
|
31
31
|
"@lezer/highlight": "^1.0.0"
|
|
32
32
|
},
|
|
33
33
|
"devDependencies": {
|
|
34
|
-
"esbuild": "^0.25.12"
|
|
34
|
+
"esbuild": "^0.25.12",
|
|
35
|
+
"jsdom": "^27.0.1"
|
|
35
36
|
},
|
|
36
37
|
"dsh": {
|
|
37
38
|
"bundle": {
|