@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.
@@ -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 = 360
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
- return { html: source, assets }
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 相对图片 → dsh-soup 同源 img 端点绝对 URL。
268
- // 预览 markdown 时,DSH 的 MarkdownText 渲染器「禁相对链接/相对资源」,
269
- // `![x](view.jpg)` 这类相对路径图片会被直接丢弃。因此把 src 改写成
270
- // 本插件同名端点(GET /api/dsh-soup/img)的同源绝对地址,交给 <img>
271
- // 从磁盘内联加载。绝对(http/https/data 等)与根相对路径放行不处理。
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
- function imgUrl(absPath) {
285
- return hostBase() + '/api/dsh-soup/img?p=' + encodeURIComponent(absPath)
286
- }
287
- function absolutizeMarkdownImages(text, baseDir) {
288
- if (!text) return text
289
- var dir = baseDir || '/'
290
- return text.replace(MD_IMG_RE, function (m0, alt, src) {
291
- var s = String(src || '').trim()
292
- if (/^(?:[a-z][a-z0-9+.-]*:|#|\/)/i.test(s)) return m0
293
- return '![' + alt + '](' + imgUrl(posixResolve(dir, s)) + ')'
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
- var md = props.baseDir ? absolutizeMarkdownImages(text, props.baseDir) : text
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
- absolutizeMarkdownImages: absolutizeMarkdownImages,
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
- return { html: source, assets };
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 imgUrl(absPath) {
31657
- return hostBase() + "/api/dsh-soup/img?p=" + encodeURIComponent(absPath);
31658
- }
31659
- function absolutizeMarkdownImages(text, baseDir) {
31660
- if (!text) return text;
31661
- var dir = baseDir || "/";
31662
- return text.replace(MD_IMG_RE, function(m0, alt, src) {
31663
- var s = String(src || "").trim();
31664
- if (/^(?:[a-z][a-z0-9+.-]*:|#|\/)/i.test(s)) return m0;
31665
- return "![" + alt + "](" + imgUrl(posixResolve(dir, s)) + ")";
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 md = props.baseDir ? absolutizeMarkdownImages(text, props.baseDir) : text;
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
- absolutizeMarkdownImages,
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 = 360;
33937
+ var DETAILS_DEFAULT = 300;
33811
33938
  function clampW(w) {
33812
33939
  return Math.max(DETAILS_MIN, Math.min(DETAILS_MAX, w));
33813
33940
  }
@@ -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 合同:默认 360px、可拖 300–520px(直接改写 shell 网格
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.6.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": {