dsh-tiddlywiki 0.33.0 → 0.33.2
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/docs/CHANGELOG.md +15 -0
- package/lib/client.js +1 -1
- package/package.json +1 -1
- package/src/client/index.ts +3 -1
- package/src/client/tid-preview.ts +128 -39
package/docs/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,21 @@
|
|
|
4
4
|
|
|
5
5
|
> 最近几个主要版本的一句话记录(完整变更见 [Releases](https://github.com/bbqisbbq/dsh-tiddlywiki/releases) / git log)。
|
|
6
6
|
|
|
7
|
+
- **v0.33.2**(2026-10-26):**`.tid` 预览仍不生效 —— 这次是"服务还没出现就放弃"**。
|
|
8
|
+
v0.33.1 修好了 `key` 配对,但用户实测仍是纯文本,且 devtools 里 `[class*="dsh-tw-tid"]` 数量为 0(正文组件压根没渲染)。
|
|
9
|
+
根因在插件的**加载顺序**:`documentPreviews` 是另一个插件(宿主的 `dsh-client-ui-sidebar-documentpreview`)用 `ctx.reflect.provide(...)` 提供的,本插件若先加载,`ctx.get(...)` 那一瞬间拿到 `undefined`,v0.32/0.33.0/0.33.1 就**静默 return** 了 —— 界面上看不出差别(宿主用内置纯文本兜底),console 里也一条日志都没有,等于把失败藏起来。
|
|
10
|
+
现在改成**短轮询等它出现**(250ms × 40 ≈ 10s),并给每一步补 `[dsh-tiddlywiki]` 前缀的诊断日志:`ctx.get`/`ctx.slots` 是否存在、第几次尝试注册上元数据、正文是否注册进 keyed 子槽、等不到时的 warn。正文外壳加 `data-dsh-tw-tid` 标记,devtools 里一条 `document.querySelectorAll('[data-dsh-tw-tid]').length` 就能判定我们的组件到底有没有渲染。
|
|
11
|
+
守门同步改判据:不再钉"查一次就返回",改钉 `setTimeout(attempt, 250)` 的轮询、`getter('documentPreviews')` 的取值形态、以及"服务始终没出现"那条可诊断 warn —— 静默跳过从这一版起是**被测出来的**行为,不再是遗漏。
|
|
12
|
+
|
|
13
|
+
- **v0.33.1**(2026-10-26):**`.tid` 预览改为真正生效**(v0.32.0 那一版静默失效:打开 `.tid` 仍显示纯文本)。
|
|
14
|
+
根因是宿主契约里我读错的一处:DSH 的文件预览分「元数据 + keyed 正文」两半,**正文按 `key` 配对** ——
|
|
15
|
+
宿主自己写的是 `ctx.slots.register({ name: sidebar.right.tab.document, key: BODY_ID }, Body)`,而 v0.32.0 写成了 `id:`。
|
|
16
|
+
**它不报错、不告警**,宿主找不到渲染器就安静地用内置纯文本兜底,于是症状看起来像「功能没做」。
|
|
17
|
+
同一轮照抄宿主补齐另外三个形状:`priority` 是**字符串**(宿主只判 `=== builtin ? 0 : 1`,非 builtin 才压得过内置纯文本)、`title` 传**函数**(语言切换可重取)、`wrap: false`(我们产出 HTML 而不是按行折行的纯文本);
|
|
18
|
+
正文取文本也从「只认 `content.text`」改成读 `content.pages`(`loading: text-pages` 时宿主给 `{ kind: text, pages }`,每页 `{ offset, lines, text }`,按 offset 排序拼接)。
|
|
19
|
+
守门同步加严(`verify-tid-preview` 12 条):**正向**钉 `slots.register({ name, key: TID_PREVIEW_ID })`、**反向**钉「不许出现 `slots.register({ … id: TID_PREVIEW_ID })`」、外加 priority/title/wrap/loading 与 pages 读取四条。
|
|
20
|
+
另:`i18n` 键数自述 467 → 475 对齐;`profiles/desktop/package-lock.json`(npm 事故遗留、含 `link: true` 软链记账)已删除。
|
|
21
|
+
|
|
7
22
|
- **v0.33.0**(2026-10-26):**客户端文案目录接进宿主 locale 服务(⑥b 的落地形态:注册,但不搬)**。原计划把 470+ 条键整体迁进 `ctx.locale.register(ns, {zh,en})`,但官方要求命名空间先并入 `LocaleNamespaceMap` 由**编译器**按类型化键并集校验 —— 本插件客户端是 esbuild 产出的单文件、无 `.d.ts`,参与不了那个类型合并,迁过去等于**放弃 `scripts/verify-client-i18n.mjs` 现有的键名校验**,去换一个「改语言即时生效」。现在两条都不放弃:**目录仍只有一份**(`i18n-catalog.ts` 的 7 个分片,宿主与本地共用同一个对象),启动时注册进宿主 locale 服务,并把宿主的 `bind(ns)` 接到 `t()` 前面 —— **宿主优先、本地表兜底**:服务缺失(老版本 DSH)、`register` 抛错、`bind` 不存在、宿主回空串 / 回键名本身 / 回非字符串,一律退回本地表,所以最差行为等于 v0.32.0。⚠️ 每次注册尝试都会先清掉上一次的绑定:留着旧解析器会让 `t()` 继续问一个已经不存在的命名空间(这一点是写守门时被自己的用例抓出来的)。**守门**:新增 `scripts/verify-locale-host.mjs`(进 `verify:unit`,7 条)—— 命名空间常量、无服务时静默跳过、注册的必须是**同一份** `MESSAGES`(另抄一份会慢慢分叉而界面看着正常)、`register` 的 disposer 必须回收、宿主优先、四类宿主异常必须回落、`t()` 必须委托给 `translate` 且保留本地兜底、接线必须在 `safeMount` 里。
|
|
8
23
|
|
|
9
24
|
- **v0.32.0**(2026-10-26):**新增 `.tid` / `.tiddler` 文件的侧栏预览**(⑦ 的第三项)。wiki 文件夹本身就是一堆 `.tid`,而 DSH 的文件预览把未知扩展名当纯文本 —— 打开一个 `.tid` 看到的是 `title: …` 字段行加原始 wikitext,`[[链接]]`/`<<宏>>` 几乎不可读。现在注册一个预览实现:元数据用 `ctx.documentPreviews.register({ id, extensions: ['tid','tiddler'], priority, title, loading: 'text-pages' })`,正文以**同一个 id** 注入 keyed、Session 作用域的 `sidebar.right.tab.document`;正文把 wikitext POST 到本插件自己的 `/dsh-tiddlywiki/render`(铁律 #7 的净化管道)换成真 TW 渲染结果,字段行单独排一行摘要。**绝不白屏**是这个功能的硬约束:拿不到内容 → "等待文件内容…";只有字段没有正文 → 明确说明;渲染失败 → `<pre>` 原文 + 一句"渲染失败,下面是原文"。宿主没有 `documentPreviews` 服务时**什么都不做**(老版本 DSH 不受影响),且元数据与正文必须**成对**(拿不到 slots 就把已注册的元数据撤回,单边注册会让面板空白)。契约是从宿主 `app.asar` 的 `dsh-client-ui-sidebar-documentpreview/lib/client.js` 逐行核实后写的(含 `TextPreview` 的完整 props 签名与 `content` 三态),全部记在 `docs/development.md`。**守门**:新增 `scripts/verify-tid-preview.mjs`(进 `verify:unit`,10 条)——纯解析用例(字段/CRLF/没有字段必须整份当正文/只有字段/空文本)+ **成对注册**(两处必须用同一个 `TID_PREVIEW_ID` 常量、正文必须先 `slots.inject` 再 `register`)+ **绝不白屏**(`<pre>` 兜底、三种空态各自的 `t()` 文案)+ 渲染必须走 `RENDER_ENDPOINT` 且失败回 `null` 不抛 + 可选依赖的静默跳过与回滚。纯逻辑拆在 `client/tid-parse.ts`(宿主 barrel 也要导它,而宿主包里不能出现 `react`),`client/tid-preview.ts` 只放 UI。客户端 i18n 473 → 477 键。
|
package/lib/client.js
CHANGED
|
@@ -962,7 +962,7 @@ html[data-dsh-tw-active] .dshDesktopConversationSurface > :not([data-dsh-tw-view
|
|
|
962
962
|
`),n=t.indexOf(`
|
|
963
963
|
|
|
964
964
|
`),r=n<0?t:t.slice(0,n),i=n<0?``:t.slice(n+2),a=[],o,s=r.trim().length>0;for(let e of r.split(`
|
|
965
|
-
`)){if(e.trim().length===0)continue;let t=/^([^\s:][^:]*):\s?(.*)$/.exec(e);if(t===null){s=!1;continue}let n=(t[1]??``).trim(),r=(t[2]??``).trim();n.length!==0&&(n===`title`?o=r:a.push([n,r]))}return n<0&&(!s||o===void 0&&a.length===0)?{fields:[],text:t}:{...o===void 0?{}:{title:o},fields:a,text:i}}function FS(e){let t=e?.content;if(typeof t
|
|
965
|
+
`)){if(e.trim().length===0)continue;let t=/^([^\s:][^:]*):\s?(.*)$/.exec(e);if(t===null){s=!1;continue}let n=(t[1]??``).trim(),r=(t[2]??``).trim();n.length!==0&&(n===`title`?o=r:a.push([n,r]))}return n<0&&(!s||o===void 0&&a.length===0)?{fields:[],text:t}:{...o===void 0?{}:{title:o},fields:a,text:i}}function FS(e){let t=e?.content;if(typeof t==`string`)return t;if(typeof t!=`object`||!t)return``;let n=t;if(typeof n.text==`string`)return n.text;let r=n.pages,i=[];if(Array.isArray(r)){for(let e of r)if(typeof e==`object`&&e){let t=e;i.push({offset:typeof t.offset==`number`?t.offset:0,text:typeof t.text==`string`?t.text:``})}}else if(typeof r==`object`&&r){for(let[e,t]of Object.entries(r))if(typeof t==`object`&&t){let n=t;i.push({offset:Number(e)||0,text:typeof n.text==`string`?n.text:``})}}return i.sort((e,t)=>e.offset-t.offset).map(e=>e.text).join(``)}async function IS(e,t){try{let n=await fetch(De(ne,t),{method:`POST`,headers:{"content-type":`application/json`,"x-requested-with":`TiddlyWiki`},body:JSON.stringify({text:e}),signal:AbortSignal.timeout(15e3)});if(!n.ok)return null;let r=await n.text();return r.trim().length>0?r:null}catch{return null}}function LS(){return function(e){let t=FS(e),n=c.useMemo(()=>PS(t),[t]),[r,i]=c.useState(null),[a,o]=c.useState(!1);c.useEffect(()=>{let e=!0;return o(!1),n.text.trim().length===0?(i(null),()=>{e=!1}):(IS(n.text).then(t=>{e&&(t===null?(o(!0),i(null)):i(t))}),()=>{e=!1})},[n.text]);let s=[];n.title!==void 0&&n.title.length>0&&s.push(c.createElement(`div`,{key:`title`,className:`dsh-tw-tid-title`},`📄 ${n.title}`)),n.fields.length>0&&s.push(c.createElement(`div`,{key:`fields`,className:`dsh-tw-tid-fields`},n.fields.map(([e,t])=>c.createElement(`span`,{key:e,className:`dsh-tw-tid-field`},`${e}: ${t.length>60?`${t.slice(0,60)}…`:t}`))));let l;return l=t.trim().length===0?c.createElement(`div`,{className:`dsh-tw-tid-empty`},S(`frame.tidPreviewWaiting`)):n.text.trim().length===0?c.createElement(`div`,{className:`dsh-tw-tid-empty`},S(`frame.tidPreviewNoBody`)):r===null?c.createElement(`div`,{className:`dsh-tw-tid-raw`},a?c.createElement(`div`,{className:`dsh-tw-tid-note`},S(`frame.tidPreviewFallback`)):null,c.createElement(`pre`,null,n.text)):c.createElement(`div`,{className:`dsh-tw-tid-body`,dangerouslySetInnerHTML:{__html:r}}),c.createElement(`div`,{className:`dsh-tw-tid-preview`,"data-dsh-tw-tid":jS},...s,l)}}function RS(e){let t=[],n=typeof e.get==`function`?e.get.bind(e):void 0;if(console.info(`[dsh-tiddlywiki] tid preview: ctx.get=`+(n===void 0?`missing`:`present`)+`, ctx.slots=`+(e.slots===void 0?`missing`:`present`)),n===void 0)return t;let r=0,i,a=!1,o=()=>{if(a)return;r+=1;let s=n(`documentPreviews`);if(s==null){if(r>=40){console.warn(`[dsh-tiddlywiki] tid preview: documentPreviews never appeared after `+r+` attempts; .tid files will fall back to the builtin plain-text preview`);return}i=setTimeout(o,250);return}a=!0;try{t.push(s.register({id:jS,extensions:[...MS],priority:`external`,title:()=>S(`frame.tidPreviewTitle`),loading:`text-pages`,wrap:!1})),console.info(`[dsh-tiddlywiki] tid preview: metadata registered on attempt `+r+` (id=dsh-tiddlywiki-tid, suffixes=`+MS.join(`/`)+`)`)}catch(e){console.warn(`[dsh-tiddlywiki] tid preview registration failed:`,e);return}let c=e.slots;if(c===void 0){console.warn(`[dsh-tiddlywiki] tid preview: no ctx.slots; rolling back the metadata registration`);for(let e of t.splice(0))e();return}let l=c.inject(NS,()=>c.register({name:NS,key:jS},LS()));l===void 0?console.warn(`[dsh-tiddlywiki] tid preview: body registration failed (slot sidebar.right.tab.document)`):(t.push(l),console.info(`[dsh-tiddlywiki] tid preview: body registered into sidebar.right.tab.document with key=dsh-tiddlywiki-tid`))};return o(),[()=>{a=!0,i!==void 0&&clearTimeout(i);for(let e of t.splice(0))e()}]}const zS=[`slots`];function BS(e){try{let t=[],n=!1,r=()=>{n=!0;for(let e of t.splice(0))e()},i=(e,t)=>{try{t()}catch(t){console.error(`[dsh-tiddlywiki] ${e} failed:`,t)}};i(`styles`,()=>t.push(T()));let a=new ee,o,s;if(i(`quick-note widget`,()=>{o=Ub(),t.push(()=>o?.dispose())}),i(`sync controller`,()=>{s=Kb(),t.push(()=>s?.dispose())}),i(`sidebar entry`,()=>t.push(st(a))),i(`center panel`,()=>t.push(tn(a))),o!==void 0&&s!==void 0){let e=o,n=s;i(`knowledge FAB`,()=>t.push(Xb(a,e,n)))}i(`editor popup`,()=>t.push(mn)),e.slots!==void 0&&Le().then(r=>{if(n)return;let a=o;r.showQuickNoteDock&&a!==void 0&&i(`quick-note dock`,()=>{let n=e.slots?.inject(`conversation.input.dock`,()=>e.slots?.register({name:`conversation.input.dock`,id:`quick-note`,order:8,label:S(`chrome.dockQuickNote`)},Qb(a)));n!==void 0&&t.push(n)}),i(`wiki scope seat`,()=>{let n=e.slots?.inject(`conversation.input.left`,()=>e.slots?.register({name:`conversation.input.left`,id:`wiki-scope`,order:9,label:S(`chrome.scopeSlotLabel`)},ox()));n!==void 0&&t.push(n)}),i(`session summary tab`,()=>{let n=dx(e.slots,r);n!==void 0&&t.push(n)})}).catch(e=>{console.error(`[dsh-tiddlywiki] ui config load failed:`,e)}),i(`rightbar mount`,()=>{let r,i=a=>{let o=e.get?.(`sidebarRightTabs`);if(o===void 0){a<6&&!n&&(r=window.setTimeout(()=>i(a+1),500*(a+1)));return}Le().then(r=>{if(n||!r.showRightbarTab)return;let i=AS(o,e.slots);i!==void 0&&t.push(i)}).catch(e=>{console.error(`[dsh-tiddlywiki] rightbar config load failed:`,e)})};t.push(()=>{r!==void 0&&window.clearTimeout(r)}),i(0)}),i(`tool views`,()=>{e.slots!==void 0&&t.push(...TS(e.slots))}),i(`wiki link interceptor`,()=>t.push(bS())),i(`tid preview`,()=>t.push(...RS(e))),i(`locale registration`,()=>t.push(...p(e.get))),i(`settings section`,()=>{let n=e.slots?.inject(`settings.section`,()=>e.slots?.register({name:`settings.section`,id:`dsh-tiddlywiki`,order:50,label:S(`frame.settingsSection`)},Vx));n!==void 0&&t.push(n)}),e.effect===void 0?window.addEventListener(`pagehide`,r,{once:!0}):e.effect(()=>r,`dsh-tiddlywiki: client mount`)}catch(e){console.error(`[dsh-tiddlywiki] client half failed to start:`,e)}}exports.apply=BS,exports.inject=zS,exports.name=`dsh-tiddlywiki/client`;
|
|
966
966
|
return module.exports;
|
|
967
967
|
}
|
|
968
968
|
});
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-tiddlywiki",
|
|
3
3
|
"description": "把 TiddlyWiki 5 变成你和 Agent 共用的本地文档中心(纯文本 + git):你在 DSH 界面里阅读、写作、记日记,Agent 用 16 个 tiddlywiki_* 工具读写同一批笔记;每条笔记都是你能读、能改、能审计、能随时带走的文件——不是自动记忆插件,不做后台抓取。可选一键发布到微信公众号草稿箱。 Turn TiddlyWiki 5 into a document center you and your agent share, in plain text under git — a knowledge base you can read, edit, audit and take with you, not an auto-memory plugin. Mounts via the official dsh plugin system; no DSH source changes.",
|
|
4
|
-
"version": "0.33.
|
|
4
|
+
"version": "0.33.2",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"dsh",
|
package/src/client/index.ts
CHANGED
|
@@ -41,7 +41,9 @@ interface ClientContextFace {
|
|
|
41
41
|
slots?: {
|
|
42
42
|
inject(name: string, register: () => unknown): (() => void) | undefined
|
|
43
43
|
register(
|
|
44
|
-
|
|
44
|
+
// keyed 子槽(如 `sidebar.right.tab.document`)按 `key` 配对;普通座位用 `id`。
|
|
45
|
+
// 两者都可选,由调用点按座位语义给其中一个(v0.33.1:`.tid` 预览就是 keyed 的)。
|
|
46
|
+
opts: { name: string; id?: string; key?: string; order?: number; label?: string | (() => string) },
|
|
45
47
|
component: unknown,
|
|
46
48
|
): () => void
|
|
47
49
|
}
|
|
@@ -11,10 +11,15 @@
|
|
|
11
11
|
*
|
|
12
12
|
* 契约(2026-10-26 从宿主 `app.asar` 的
|
|
13
13
|
* `dsh-client-ui-sidebar-documentpreview/lib/client.js` 逐行核实,见 docs/development.md):
|
|
14
|
-
* · 元数据:`ctx.documentPreviews.register({ id, extensions,
|
|
14
|
+
* · 元数据:`ctx.documentPreviews.register({ id, extensions, priority, title, loading, wrap })`,
|
|
15
15
|
* **按 id 去重**,`binaryExtensions` 必须在 `extensions` 内;
|
|
16
|
-
*
|
|
17
|
-
*
|
|
16
|
+
* `priority` 是**字符串**(宿主只判 `=== 'builtin' ? 0 : 1`,非 builtin 压过内置);
|
|
17
|
+
* `title` 宿主自己传的是**函数**;`wrap` 表示是否按纯文本换行(我们要 HTML,用 false)。
|
|
18
|
+
* · 正文:注入 keyed、Session 作用域的 `sidebar.right.tab.document`,**按 `key` 配对**:
|
|
19
|
+
* `ctx.slots.register({ name: 'sidebar.right.tab.document', key: <与元数据同一个 id> }, Body)`。
|
|
20
|
+
* ⚠️ 用 `id` 而不是 `key` 会**静默退化成内置纯文本**(v0.32.0 的实际症状)。
|
|
21
|
+
* · 正文 props:`{ content, useTabInfo, … }`,`loading: 'text-pages'` 时
|
|
22
|
+
* `content = { kind: 'text', pages }`,每页 `{ offset, lines, text }`。
|
|
18
23
|
*
|
|
19
24
|
* NEVER A BLANK PANEL
|
|
20
25
|
* -------------------
|
|
@@ -33,12 +38,38 @@ import { parseTidText, TID_EXTENSIONS, TID_PREVIEW_ID, TID_PREVIEW_SLOT } from '
|
|
|
33
38
|
export { parseTidText, TID_EXTENSIONS, TID_PREVIEW_ID, TID_PREVIEW_SLOT } from './tid-parse.ts'
|
|
34
39
|
export type { ParsedTid } from './tid-parse.ts'
|
|
35
40
|
|
|
36
|
-
/**
|
|
41
|
+
/**
|
|
42
|
+
* 从正文 props 里取文件文本。
|
|
43
|
+
*
|
|
44
|
+
* `loading: 'text-pages'` 时宿主给的是 `content = { kind: 'text', pages }`,每页是
|
|
45
|
+
* `{ offset, lines, text }`(见宿主 `documentpreview/lib/client.js` 的 `linesOf`)。
|
|
46
|
+
* `pages` 既可能是数组、也可能是 `{ offset: page }` 记录,所以两种都收;
|
|
47
|
+
* 另外也支持更简单的 `content.text`(某些加载模式直接给整段)。
|
|
48
|
+
*/
|
|
37
49
|
function textOf(props: unknown): string {
|
|
38
|
-
const content = (props as { content?:
|
|
39
|
-
if (typeof content
|
|
40
|
-
|
|
41
|
-
|
|
50
|
+
const content = (props as { content?: unknown } | undefined)?.content
|
|
51
|
+
if (typeof content === 'string') return content
|
|
52
|
+
if (content === null || typeof content !== 'object') return ''
|
|
53
|
+
const bag = content as { text?: unknown; pages?: unknown }
|
|
54
|
+
if (typeof bag.text === 'string') return bag.text
|
|
55
|
+
const rawPages = bag.pages
|
|
56
|
+
const list: Array<{ offset: number; text: string }> = []
|
|
57
|
+
if (Array.isArray(rawPages)) {
|
|
58
|
+
for (const page of rawPages) {
|
|
59
|
+
if (page !== null && typeof page === 'object') {
|
|
60
|
+
const p = page as { offset?: unknown; text?: unknown }
|
|
61
|
+
list.push({ offset: typeof p.offset === 'number' ? p.offset : 0, text: typeof p.text === 'string' ? p.text : '' })
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
} else if (rawPages !== null && typeof rawPages === 'object') {
|
|
65
|
+
for (const [offset, page] of Object.entries(rawPages as Record<string, unknown>)) {
|
|
66
|
+
if (page !== null && typeof page === 'object') {
|
|
67
|
+
const p = page as { text?: unknown }
|
|
68
|
+
list.push({ offset: Number(offset) || 0, text: typeof p.text === 'string' ? p.text : '' })
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return list.sort((a, b) => a.offset - b.offset).map((p) => p.text).join('')
|
|
42
73
|
}
|
|
43
74
|
|
|
44
75
|
/** 把一段 wikitext 换成宿主**净化过**的 HTML;失败回 null(调用方退回原文)。 */
|
|
@@ -117,7 +148,7 @@ export function createTidPreviewBody(): (props: unknown) => React.ReactElement {
|
|
|
117
148
|
)
|
|
118
149
|
}
|
|
119
150
|
|
|
120
|
-
return React.createElement('div', { className: 'dsh-tw-tid-preview' }, ...head, body)
|
|
151
|
+
return React.createElement('div', { className: 'dsh-tw-tid-preview', 'data-dsh-tw-tid': TID_PREVIEW_ID }, ...head, body)
|
|
121
152
|
}
|
|
122
153
|
}
|
|
123
154
|
|
|
@@ -127,8 +158,14 @@ interface DocumentPreviewsFace {
|
|
|
127
158
|
id: string
|
|
128
159
|
extensions: string[]
|
|
129
160
|
binaryExtensions?: string[]
|
|
130
|
-
|
|
131
|
-
|
|
161
|
+
/**
|
|
162
|
+
* **字符串**,不是数字:宿主只判 `priority === 'builtin' ? 0 : 1`,
|
|
163
|
+
* rank 高的先命中,也就是「非 builtin(外部实现)压过内置」。宿主内置的
|
|
164
|
+
* 纯文本/Markdown 都是 `'builtin'`,所以我们要声明 `'external'`。
|
|
165
|
+
*/
|
|
166
|
+
priority?: 'builtin' | 'external'
|
|
167
|
+
/** 展示名。宿主自己的实现传的是**函数**(`() => t('viewer.text')`),照抄。 */
|
|
168
|
+
title?: string | (() => string)
|
|
132
169
|
loading?: 'text-pages' | 'bytes-complete' | 'renderer'
|
|
133
170
|
wrap?: boolean
|
|
134
171
|
}): () => void
|
|
@@ -139,44 +176,96 @@ interface PreviewCtx {
|
|
|
139
176
|
get?: (name: string) => unknown
|
|
140
177
|
slots?: {
|
|
141
178
|
inject(name: string, register: () => unknown): (() => void) | undefined
|
|
142
|
-
|
|
179
|
+
/**
|
|
180
|
+
* keyed 子槽按 **`key`** 配对(宿主写的是 `register({ name, key: BODY_ID }, C)`)——
|
|
181
|
+
* 不是 `id`。用 `id` 会静默退化成内置纯文本,正是 v0.32.0 的实际症状。
|
|
182
|
+
*/
|
|
183
|
+
register(opts: { name: string; key: string }, component: unknown): (() => void) | undefined
|
|
143
184
|
}
|
|
144
185
|
}
|
|
145
186
|
|
|
146
187
|
/**
|
|
147
188
|
* 注册 `.tid` 预览(元数据 + 正文**成对**)。
|
|
148
189
|
*
|
|
149
|
-
*
|
|
150
|
-
*
|
|
190
|
+
* 为什么要「等」而不是查一次:`documentPreviews` 是**另一个插件**(宿主的
|
|
191
|
+
* `dsh-client-ui-sidebar-documentpreview`)用 `ctx.reflect.provide('documentPreviews', …)`
|
|
192
|
+
* 提供的。如果本插件先加载,`ctx.get('documentPreviews')` 那一刻还是 `undefined` ——
|
|
193
|
+
* v0.32/0.33.0 就是在这里**静默放弃**的(用户看到的仍是内置纯文本,console 里
|
|
194
|
+
* 一条日志都没有,排查成本极高)。现在改成**短轮询**(250ms × 40 ≈ 10s):
|
|
195
|
+
* 服务一出现就注册;始终没出现才记一条 warn 说明原因。
|
|
196
|
+
*
|
|
197
|
+
* 诊断日志都带 `[dsh-tiddlywiki]` 前缀,方便在 devtools 里一眼认出是我们的。
|
|
151
198
|
*/
|
|
152
199
|
export function registerTidPreview(ctx: PreviewCtx): Array<() => void> {
|
|
153
200
|
const disposers: Array<() => void> = []
|
|
154
|
-
const
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
previews.register({
|
|
159
|
-
id: TID_PREVIEW_ID,
|
|
160
|
-
extensions: [...TID_EXTENSIONS],
|
|
161
|
-
priority: 100,
|
|
162
|
-
title: t('frame.tidPreviewTitle'),
|
|
163
|
-
loading: 'text-pages',
|
|
164
|
-
}),
|
|
165
|
-
)
|
|
166
|
-
} catch (err) {
|
|
167
|
-
// 开发者日志,不是用户文案(与仓库其它模块的 console.warn 同一性质)。
|
|
168
|
-
console.warn('[dsh-tiddlywiki] tid preview registration failed:', err)
|
|
201
|
+
const getter = typeof ctx.get === 'function' ? ctx.get.bind(ctx) : undefined
|
|
202
|
+
console.info('[dsh-tiddlywiki] tid preview: ctx.get=' + (getter === undefined ? 'missing' : 'present') + ', ctx.slots=' + (ctx.slots === undefined ? 'missing' : 'present'))
|
|
203
|
+
if (getter === undefined) {
|
|
204
|
+
// 没有服务读取器 ⇒ 这个宿主版本给不了 documentPreviews,按设计跳过。
|
|
169
205
|
return disposers
|
|
170
206
|
}
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
207
|
+
|
|
208
|
+
let attempts = 0
|
|
209
|
+
let timer: ReturnType<typeof setTimeout> | undefined
|
|
210
|
+
let done = false
|
|
211
|
+
|
|
212
|
+
const attempt = (): void => {
|
|
213
|
+
if (done) return
|
|
214
|
+
attempts += 1
|
|
215
|
+
const previews = getter('documentPreviews') as DocumentPreviewsFace | undefined
|
|
216
|
+
if (previews === undefined || previews === null) {
|
|
217
|
+
if (attempts >= 40) {
|
|
218
|
+
// 开发者日志(客户端守门禁止中文字面量,故一律英文)
|
|
219
|
+
console.warn('[dsh-tiddlywiki] tid preview: documentPreviews never appeared after ' + attempts + ' attempts; .tid files will fall back to the builtin plain-text preview')
|
|
220
|
+
return
|
|
221
|
+
}
|
|
222
|
+
timer = setTimeout(attempt, 250)
|
|
223
|
+
return
|
|
224
|
+
}
|
|
225
|
+
done = true
|
|
226
|
+
try {
|
|
227
|
+
disposers.push(
|
|
228
|
+
previews.register({
|
|
229
|
+
id: TID_PREVIEW_ID,
|
|
230
|
+
extensions: [...TID_EXTENSIONS],
|
|
231
|
+
// 外部实现压过宿主的纯文本兜底(宿主按 `priority === 'builtin'` 分档)。
|
|
232
|
+
priority: 'external',
|
|
233
|
+
// 宿主自己传的是函数(`) => t('viewer.text')`),语言切换时能重取。
|
|
234
|
+
title: () => t('frame.tidPreviewTitle'),
|
|
235
|
+
loading: 'text-pages',
|
|
236
|
+
wrap: false,
|
|
237
|
+
}),
|
|
238
|
+
)
|
|
239
|
+
console.info('[dsh-tiddlywiki] tid preview: metadata registered on attempt ' + attempts + ' (id=' + TID_PREVIEW_ID + ', suffixes=' + TID_EXTENSIONS.join('/') + ')')
|
|
240
|
+
} catch (err) {
|
|
241
|
+
// 开发者日志,不是用户文案(与仓库其它模块的 console.warn 同一性质)。
|
|
242
|
+
console.warn('[dsh-tiddlywiki] tid preview registration failed:', err)
|
|
243
|
+
return
|
|
244
|
+
}
|
|
245
|
+
const slots = ctx.slots
|
|
246
|
+
if (slots === undefined) {
|
|
247
|
+
// 元数据已注册但没有正文 → 宿主会拿不到渲染器,宁可整体撤回。
|
|
248
|
+
console.warn('[dsh-tiddlywiki] tid preview: no ctx.slots; rolling back the metadata registration')
|
|
249
|
+
for (const dispose of disposers.splice(0)) dispose()
|
|
250
|
+
return
|
|
251
|
+
}
|
|
252
|
+
const bodyDispose = slots.inject(TID_PREVIEW_SLOT, () =>
|
|
253
|
+
slots.register({ name: TID_PREVIEW_SLOT, key: TID_PREVIEW_ID }, createTidPreviewBody()),
|
|
254
|
+
)
|
|
255
|
+
if (bodyDispose === undefined) console.warn('[dsh-tiddlywiki] tid preview: body registration failed (slot ' + TID_PREVIEW_SLOT + ')')
|
|
256
|
+
else {
|
|
257
|
+
disposers.push(bodyDispose)
|
|
258
|
+
console.info('[dsh-tiddlywiki] tid preview: body registered into ' + TID_PREVIEW_SLOT + ' with key=' + TID_PREVIEW_ID)
|
|
259
|
+
}
|
|
176
260
|
}
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
261
|
+
|
|
262
|
+
attempt()
|
|
263
|
+
// 清理:停掉还在等服务的定时器 + 回收已注册的两半。
|
|
264
|
+
return [
|
|
265
|
+
() => {
|
|
266
|
+
done = true
|
|
267
|
+
if (timer !== undefined) clearTimeout(timer)
|
|
268
|
+
for (const dispose of disposers.splice(0)) dispose()
|
|
269
|
+
},
|
|
270
|
+
]
|
|
182
271
|
}
|