@lyhue1991/dsh-soup 0.6.0 → 0.7.0
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/client/explorer-view.js +1 -1
- package/lib/client/html-preview.js +120 -1
- package/lib/client/preview-renderers.js +35 -22
- package/lib/client.js +145 -18
- package/lib/client.main.js +1 -1
- package/package.json +4 -3
|
@@ -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,
|
|
@@ -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
|
|
|
@@ -264,13 +263,16 @@ export function createPreviewRenderers(ctx) {
|
|
|
264
263
|
var CSV_MAX_COLS = 64
|
|
265
264
|
|
|
266
265
|
// ------------------------------------------------------------------
|
|
267
|
-
// Markdown 相对图片 →
|
|
268
|
-
//
|
|
269
|
-
//
|
|
270
|
-
//
|
|
271
|
-
//
|
|
266
|
+
// Markdown 相对图片 → 官方 /api/file 文件路由词汇表(pathImages)。
|
|
267
|
+
// DSH 的 MarkdownText 渲染器只放行 http/https/mailto URL 与
|
|
268
|
+
// pathImages/fileImages 词汇表改写结果:桌面端 WebView 的页面基址是
|
|
269
|
+
// 自定义协议 dsh-app://app,把 src 改写成自家 img 端点会被
|
|
270
|
+
// sanitizeUrl 直接丢弃(alt 文本兜底);浏览器打开时才是 http。
|
|
271
|
+
// 因此改为向 MarkdownText 提供 pathImages 词汇表:相对路径以预览
|
|
272
|
+
// 文件所在目录为基解析成绝对路径,经官方 api/file 路由(浏览器为
|
|
273
|
+
// http 同源、桌面为 dsh-app://app/api/file,均在官方白名单内,
|
|
274
|
+
// 鉴权 cookie 由协议转发器注入)返回给 <img> 加载。
|
|
272
275
|
// ------------------------------------------------------------------
|
|
273
|
-
var MD_IMG_RE = /!\[([^\]]*)\]\(([^)\s]+)(?:\s+["'][^"']*["'])?\)/g
|
|
274
276
|
function posixResolve(dir, rel) {
|
|
275
277
|
var combined = (String(dir || '/').replace(/\/+$/, '') || '/') + '/' + String(rel)
|
|
276
278
|
var out = []
|
|
@@ -281,17 +283,24 @@ export function createPreviewRenderers(ctx) {
|
|
|
281
283
|
})
|
|
282
284
|
return '/' + out.join('/')
|
|
283
285
|
}
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
var
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
286
|
+
/** pathImages.resolve:本地路径 → 官方 api/file URL;非本地路径返回 undefined。 */
|
|
287
|
+
function localPathImageUrl(baseDir, value) {
|
|
288
|
+
var raw = String(value || '')
|
|
289
|
+
// 与 DSH localPathMediaUrl 同规:剥离 ?# 后缀,URL 转义解码一次。
|
|
290
|
+
var bare = raw.split(/[?#]/)[0]
|
|
291
|
+
var decoded
|
|
292
|
+
try { decoded = decodeURIComponent(bare) } catch (e) { return undefined }
|
|
293
|
+
if (!decoded || /[\u0000-\u001f\u007f]/.test(decoded)) return undefined
|
|
294
|
+
// 协议 URL(http/https/data 等)与 UNC 路径不属于本地路径词汇表,
|
|
295
|
+
// 留给官方 imageSource 处理;Windows 盘符路径按官方例外放行。
|
|
296
|
+
if (/^[a-z][a-z\d+.-]*:/i.test(decoded) && !/^[a-z]:[\\/]/i.test(decoded)) return undefined
|
|
297
|
+
if (/^[/\\]{2}/.test(decoded)) return undefined
|
|
298
|
+
var abs = decoded.charAt(0) === '/' || /^[a-z]:[\\/]/i.test(decoded)
|
|
299
|
+
? decoded
|
|
300
|
+
: posixResolve(baseDir || '/', decoded)
|
|
301
|
+
try {
|
|
302
|
+
return new URL('api/file?path=' + encodeURIComponent(abs), document.baseURI).href
|
|
303
|
+
} catch (e) { return undefined }
|
|
295
304
|
}
|
|
296
305
|
|
|
297
306
|
var MarkdownRenderBoundary = React.Component
|
|
@@ -403,7 +412,11 @@ export function createPreviewRenderers(ctx) {
|
|
|
403
412
|
if (MarkdownText) {
|
|
404
413
|
// Keep the original MarkdownText source contract visible for host integrations.
|
|
405
414
|
// create(MarkdownText, { text: md })
|
|
406
|
-
|
|
415
|
+
// 相对图片经 pathImages 词汇表交给官方 api/file 路由(见 localPathImageUrl 注释)。
|
|
416
|
+
var pathImages = props.baseDir
|
|
417
|
+
? { resolve: function (value) { return localPathImageUrl(props.baseDir, value) } }
|
|
418
|
+
: undefined
|
|
419
|
+
var md = text
|
|
407
420
|
var lines = String(md || '').split(/\r?\n/)
|
|
408
421
|
var chunks = []
|
|
409
422
|
var normal = []
|
|
@@ -411,7 +424,7 @@ export function createPreviewRenderers(ctx) {
|
|
|
411
424
|
function pushNormal() {
|
|
412
425
|
if (!normal.length) return
|
|
413
426
|
var value = normal.join('\n')
|
|
414
|
-
if (value.trim() !== '') chunks.push(create(MarkdownRenderBoundary, { key: 'md-' + chunks.length, text: value }, create(MarkdownText, { text: value })))
|
|
427
|
+
if (value.trim() !== '') chunks.push(create(MarkdownRenderBoundary, { key: 'md-' + chunks.length, text: value }, create(MarkdownText, { text: value, pathImages: pathImages })))
|
|
415
428
|
normal = []
|
|
416
429
|
}
|
|
417
430
|
function pushTable(headerLine) {
|
|
@@ -421,7 +434,7 @@ export function createPreviewRenderers(ctx) {
|
|
|
421
434
|
}
|
|
422
435
|
var headers = splitRow(headerLine)
|
|
423
436
|
var renderCell = function (cell, key) {
|
|
424
|
-
return create(MarkdownRenderBoundary, { key: key, text: cell }, create(MarkdownText, { text: cell }))
|
|
437
|
+
return create(MarkdownRenderBoundary, { key: key, text: cell }, create(MarkdownText, { text: cell, pathImages: pathImages }))
|
|
425
438
|
}
|
|
426
439
|
var head = create('tr', null, headers.map(function (cell, hi) {
|
|
427
440
|
return create('th', { key: hi, className: 'dfv-th' }, renderCell(cell, 'th-' + hi))
|
|
@@ -808,7 +821,7 @@ export function createPreviewRenderers(ctx) {
|
|
|
808
821
|
return {
|
|
809
822
|
codeMirrorLanguage: codeMirrorLanguage,
|
|
810
823
|
jsonColorNodes: jsonColorNodes,
|
|
811
|
-
|
|
824
|
+
localPathImageUrl: localPathImageUrl,
|
|
812
825
|
TextEditor: TextEditor,
|
|
813
826
|
MarkdownPreview: MarkdownPreview,
|
|
814
827
|
PdfPreview: PdfPreview,
|
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) {
|
|
@@ -31639,7 +31757,6 @@
|
|
|
31639
31757
|
}
|
|
31640
31758
|
var CSV_MAX_ROWS = 500;
|
|
31641
31759
|
var CSV_MAX_COLS = 64;
|
|
31642
|
-
var MD_IMG_RE = /!\[([^\]]*)\]\(([^)\s]+)(?:\s+["'][^"']*["'])?\)/g;
|
|
31643
31760
|
function posixResolve(dir, rel) {
|
|
31644
31761
|
var combined = (String(dir || "/").replace(/\/+$/, "") || "/") + "/" + String(rel);
|
|
31645
31762
|
var out = [];
|
|
@@ -31653,17 +31770,24 @@
|
|
|
31653
31770
|
});
|
|
31654
31771
|
return "/" + out.join("/");
|
|
31655
31772
|
}
|
|
31656
|
-
function
|
|
31657
|
-
|
|
31658
|
-
|
|
31659
|
-
|
|
31660
|
-
|
|
31661
|
-
|
|
31662
|
-
|
|
31663
|
-
|
|
31664
|
-
|
|
31665
|
-
|
|
31666
|
-
|
|
31773
|
+
function localPathImageUrl(baseDir, value) {
|
|
31774
|
+
var raw = String(value || "");
|
|
31775
|
+
var bare = raw.split(/[?#]/)[0];
|
|
31776
|
+
var decoded;
|
|
31777
|
+
try {
|
|
31778
|
+
decoded = decodeURIComponent(bare);
|
|
31779
|
+
} catch (e) {
|
|
31780
|
+
return void 0;
|
|
31781
|
+
}
|
|
31782
|
+
if (!decoded || /[\u0000-\u001f\u007f]/.test(decoded)) return void 0;
|
|
31783
|
+
if (/^[a-z][a-z\d+.-]*:/i.test(decoded) && !/^[a-z]:[\\/]/i.test(decoded)) return void 0;
|
|
31784
|
+
if (/^[/\\]{2}/.test(decoded)) return void 0;
|
|
31785
|
+
var abs = decoded.charAt(0) === "/" || /^[a-z]:[\\/]/i.test(decoded) ? decoded : posixResolve(baseDir || "/", decoded);
|
|
31786
|
+
try {
|
|
31787
|
+
return new URL("api/file?path=" + encodeURIComponent(abs), document.baseURI).href;
|
|
31788
|
+
} catch (e) {
|
|
31789
|
+
return void 0;
|
|
31790
|
+
}
|
|
31667
31791
|
}
|
|
31668
31792
|
var MarkdownRenderBoundary = React.Component ? class MarkdownRenderBoundary extends React.Component {
|
|
31669
31793
|
constructor(props) {
|
|
@@ -31785,7 +31909,7 @@
|
|
|
31785
31909
|
let pushNormal = function() {
|
|
31786
31910
|
if (!normal.length) return;
|
|
31787
31911
|
var value = normal.join("\n");
|
|
31788
|
-
if (value.trim() !== "") chunks.push(create(MarkdownRenderBoundary, { key: "md-" + chunks.length, text: value }, create(MarkdownText, { text: value })));
|
|
31912
|
+
if (value.trim() !== "") chunks.push(create(MarkdownRenderBoundary, { key: "md-" + chunks.length, text: value }, create(MarkdownText, { text: value, pathImages })));
|
|
31789
31913
|
normal = [];
|
|
31790
31914
|
}, pushTable = function(headerLine) {
|
|
31791
31915
|
var splitRow = function(value) {
|
|
@@ -31796,7 +31920,7 @@
|
|
|
31796
31920
|
};
|
|
31797
31921
|
var headers = splitRow(headerLine);
|
|
31798
31922
|
var renderCell = function(cell, key) {
|
|
31799
|
-
return create(MarkdownRenderBoundary, { key, text: cell }, create(MarkdownText, { text: cell }));
|
|
31923
|
+
return create(MarkdownRenderBoundary, { key, text: cell }, create(MarkdownText, { text: cell, pathImages }));
|
|
31800
31924
|
};
|
|
31801
31925
|
var head = create("tr", null, headers.map(function(cell, hi) {
|
|
31802
31926
|
return create("th", { key: hi, className: "dfv-th" }, renderCell(cell, "th-" + hi));
|
|
@@ -31824,7 +31948,10 @@
|
|
|
31824
31948
|
)
|
|
31825
31949
|
));
|
|
31826
31950
|
};
|
|
31827
|
-
var
|
|
31951
|
+
var pathImages = props.baseDir ? { resolve: function(value) {
|
|
31952
|
+
return localPathImageUrl(props.baseDir, value);
|
|
31953
|
+
} } : void 0;
|
|
31954
|
+
var md = text;
|
|
31828
31955
|
var lines = String(md || "").split(/\r?\n/);
|
|
31829
31956
|
var chunks = [];
|
|
31830
31957
|
var normal = [];
|
|
@@ -32234,7 +32361,7 @@
|
|
|
32234
32361
|
return {
|
|
32235
32362
|
codeMirrorLanguage,
|
|
32236
32363
|
jsonColorNodes,
|
|
32237
|
-
|
|
32364
|
+
localPathImageUrl,
|
|
32238
32365
|
TextEditor,
|
|
32239
32366
|
MarkdownPreview,
|
|
32240
32367
|
PdfPreview,
|
|
@@ -33807,7 +33934,7 @@
|
|
|
33807
33934
|
var uploadTargetDir = null;
|
|
33808
33935
|
var DETAILS_MIN = 300;
|
|
33809
33936
|
var DETAILS_MAX = 520;
|
|
33810
|
-
var DETAILS_DEFAULT =
|
|
33937
|
+
var DETAILS_DEFAULT = 300;
|
|
33811
33938
|
function clampW(w) {
|
|
33812
33939
|
return Math.max(DETAILS_MIN, Math.min(DETAILS_MAX, w));
|
|
33813
33940
|
}
|
package/lib/client.main.js
CHANGED
|
@@ -17,7 +17,7 @@ import { createExplorerView } from './client/explorer-view.js'
|
|
|
17
17
|
* 布局:注册进 shell 原生的 `details` 右列(并列网格,非悬浮),
|
|
18
18
|
* 有 Session log 时在它右侧放 📁;空白新会话则在输入框上方、
|
|
19
19
|
* workspace/preset 控件行的右端提供同一入口。
|
|
20
|
-
* 宽度对齐宿主 details 合同:默认
|
|
20
|
+
* 宽度对齐宿主 details 合同:默认 300px、可拖 300–520px(直接改写 shell 网格
|
|
21
21
|
* 的 details 轨,并用 MutationObserver 在 shell 重渲染后维持宽度)。
|
|
22
22
|
*
|
|
23
23
|
* 与宿主通信:走同源 HTTP 路由 `/api/dsh-soup`(POST JSON),
|
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.0",
|
|
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/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": {
|