D164: 登记Tolaria光湖定制版源码补丁
This commit is contained in:
parent
64340534c0
commit
a3cce320ab
44
products/tolaria-guanghu/README.hdlp
Normal file
44
products/tolaria-guanghu/README.hdlp
Normal file
@ -0,0 +1,44 @@
|
||||
---
|
||||
id: DEV-TOL-SRC-001
|
||||
protocol: HLDP
|
||||
product: Tolaria光湖定制版
|
||||
upstream: https://github.com/refactoringhq/tolaria
|
||||
upstream_commit: af6891b0af725a219427eb0ff056cbc719e40812
|
||||
local_commit: 9bd612c
|
||||
status: installed
|
||||
maintainer: ICE-GL-ZY001
|
||||
receipt: CC-074
|
||||
---
|
||||
|
||||
# Tolaria光湖定制版 · 源码补丁入口
|
||||
|
||||
## 当前补丁
|
||||
|
||||
`patches/0001-feat-add-plain-click-wikilinks-and-Guanghu-portal-bl.patch`
|
||||
|
||||
包含:
|
||||
|
||||
1. 富文本wikilink普通鼠标单击跳转。
|
||||
2. 中文、Emoji与Notion长编号后缀回归测试。
|
||||
3. `guanghu-portal` Markdown持久化组件。
|
||||
4. 光湖入口卡富文本渲染样式。
|
||||
|
||||
## 恢复方法
|
||||
|
||||
```bash
|
||||
git clone https://github.com/refactoringhq/tolaria.git
|
||||
cd tolaria
|
||||
git checkout af6891b0af725a219427eb0ff056cbc719e40812
|
||||
git am /path/to/0001-feat-add-plain-click-wikilinks-and-Guanghu-portal-bl.patch
|
||||
```
|
||||
|
||||
## 已验证
|
||||
|
||||
- 4873项前端测试通过。
|
||||
- TypeScript/Vite生产构建通过。
|
||||
- Tauri macOS原生构建通过。
|
||||
- 安装位置:`/Applications/Tolaria.app`
|
||||
- 官方回退版:`/Applications/Tolaria-Official-2026.7.1.app`
|
||||
|
||||
> 认知链与桌面验收见 CC-074。
|
||||
|
||||
@ -0,0 +1,391 @@
|
||||
From 9bd612ca137fb1f40b5ddd022a68a73b2f38eb98 Mon Sep 17 00:00:00 2001
|
||||
From: =?UTF-8?q?=E9=93=B8=E6=B8=8A?= <zhuyuan@guanghu.local>
|
||||
Date: Fri, 3 Jul 2026 13:43:14 +0800
|
||||
Subject: [PATCH] feat: add plain-click wikilinks and Guanghu portal block
|
||||
|
||||
---
|
||||
src/components/Editor.css | 44 +++++++++
|
||||
src/components/editorSchema.tsx | 39 ++++++++
|
||||
.../useEditorLinkActivation.test.tsx | 11 +--
|
||||
src/components/useEditorLinkActivation.ts | 2 -
|
||||
src/utils/editorDurableMarkdown.ts | 2 +
|
||||
src/utils/guanghuPortalMarkdown.test.ts | 38 ++++++++
|
||||
src/utils/guanghuPortalMarkdown.ts | 93 +++++++++++++++++++
|
||||
src/utils/wikilink.test.ts | 23 +++++
|
||||
8 files changed, 243 insertions(+), 9 deletions(-)
|
||||
create mode 100644 src/utils/guanghuPortalMarkdown.test.ts
|
||||
create mode 100644 src/utils/guanghuPortalMarkdown.ts
|
||||
|
||||
diff --git a/src/components/Editor.css b/src/components/Editor.css
|
||||
index 39866cc..499b72f 100644
|
||||
--- a/src/components/Editor.css
|
||||
+++ b/src/components/Editor.css
|
||||
@@ -22,6 +22,50 @@
|
||||
opacity: 0.55;
|
||||
}
|
||||
|
||||
+.guanghu-portal-card {
|
||||
+ width: 100%;
|
||||
+ margin: 0.5rem 0;
|
||||
+ padding: 1rem 1.1rem;
|
||||
+ border: 1px solid color-mix(in srgb, var(--colors-accent) 38%, var(--colors-border));
|
||||
+ border-radius: 0.85rem;
|
||||
+ background:
|
||||
+ radial-gradient(circle at top right, color-mix(in srgb, var(--colors-accent) 16%, transparent), transparent 48%),
|
||||
+ var(--colors-surface);
|
||||
+ box-shadow: 0 0.35rem 1.25rem color-mix(in srgb, black 10%, transparent);
|
||||
+}
|
||||
+
|
||||
+.guanghu-portal-card__eyebrow {
|
||||
+ display: flex;
|
||||
+ align-items: center;
|
||||
+ justify-content: space-between;
|
||||
+ gap: 0.75rem;
|
||||
+ margin-bottom: 0.55rem;
|
||||
+ color: var(--colors-text-muted);
|
||||
+ font-size: 0.72rem;
|
||||
+ letter-spacing: 0.08em;
|
||||
+}
|
||||
+
|
||||
+.guanghu-portal-card__status {
|
||||
+ padding: 0.18rem 0.55rem;
|
||||
+ border-radius: 999px;
|
||||
+ background: color-mix(in srgb, var(--colors-accent) 18%, transparent);
|
||||
+ color: var(--colors-text);
|
||||
+ letter-spacing: normal;
|
||||
+}
|
||||
+
|
||||
+.guanghu-portal-card__title {
|
||||
+ display: inline-flex;
|
||||
+ font-size: 1.08rem;
|
||||
+ font-weight: 650;
|
||||
+}
|
||||
+
|
||||
+.guanghu-portal-card__description {
|
||||
+ margin: 0.45rem 0 0;
|
||||
+ color: var(--colors-text-muted);
|
||||
+ font-size: 0.9rem;
|
||||
+ line-height: 1.55;
|
||||
+}
|
||||
+
|
||||
/* Breadcrumb bar: border can still react to the data attribute, but the
|
||||
breadcrumb filename/title stays visible at all times. */
|
||||
.breadcrumb-bar {
|
||||
diff --git a/src/components/editorSchema.tsx b/src/components/editorSchema.tsx
|
||||
index 769cec2..2984eff 100644
|
||||
--- a/src/components/editorSchema.tsx
|
||||
+++ b/src/components/editorSchema.tsx
|
||||
@@ -24,6 +24,7 @@ import { MATH_BLOCK_TYPE, MATH_INLINE_TYPE, renderMathToHtml } from '../utils/ma
|
||||
import { MERMAID_BLOCK_TYPE, mermaidFenceSource } from '../utils/mermaidMarkdown'
|
||||
import { TLDRAW_BLOCK_TYPE, TLDRAW_DEFAULT_HEIGHT } from '../utils/tldrawMarkdown'
|
||||
import { MARKDOWN_HIGHLIGHT_STYLE } from '../utils/markdownHighlightMarkdown'
|
||||
+import { GUANGHU_PORTAL_BLOCK_TYPE } from '../utils/guanghuPortalMarkdown'
|
||||
import type { VaultEntry } from '../types'
|
||||
import { createTolariaCodeBlockOptions } from './codeBlockOptions'
|
||||
import { NoteTitleIcon } from './NoteTitleIcon'
|
||||
@@ -313,6 +314,42 @@ const MermaidBlock = createReactBlockSpec(
|
||||
},
|
||||
)
|
||||
|
||||
+const GuanghuPortalBlock = createReactBlockSpec(
|
||||
+ {
|
||||
+ type: GUANGHU_PORTAL_BLOCK_TYPE,
|
||||
+ propSchema: {
|
||||
+ source: { default: '' },
|
||||
+ title: { default: '光湖入口' },
|
||||
+ target: { default: '' },
|
||||
+ description: { default: '' },
|
||||
+ status: { default: '' },
|
||||
+ },
|
||||
+ content: 'none',
|
||||
+ },
|
||||
+ {
|
||||
+ runsBefore: ['codeBlock'],
|
||||
+ render: props => (
|
||||
+ <article className="guanghu-portal-card" contentEditable={false}>
|
||||
+ <div className="guanghu-portal-card__eyebrow">
|
||||
+ <span>HLDP · 光湖入口</span>
|
||||
+ {props.block.props.status ? (
|
||||
+ <span className="guanghu-portal-card__status">{props.block.props.status}</span>
|
||||
+ ) : null}
|
||||
+ </div>
|
||||
+ <span
|
||||
+ className="guanghu-portal-card__title wikilink"
|
||||
+ data-target={props.block.props.target}
|
||||
+ >
|
||||
+ {props.block.props.title}
|
||||
+ </span>
|
||||
+ {props.block.props.description ? (
|
||||
+ <p className="guanghu-portal-card__description">{props.block.props.description}</p>
|
||||
+ ) : null}
|
||||
+ </article>
|
||||
+ ),
|
||||
+ },
|
||||
+)
|
||||
+
|
||||
export function mediaBlockPropsForPreviewRuntime<T extends MediaBlockPreviewProps>(
|
||||
props: T,
|
||||
externalMediaPreview: boolean,
|
||||
@@ -412,6 +449,7 @@ const TldrawBlock = createReactBlockSpec(
|
||||
const codeBlock = createCodeBlockSpec(createTolariaCodeBlockOptions())
|
||||
const audioBlock = AudioBlockSpec()
|
||||
const mathBlock = MathBlock()
|
||||
+const guanghuPortalBlock = GuanghuPortalBlock()
|
||||
const mermaidBlock = MermaidBlock()
|
||||
const tldrawBlock = TldrawBlock()
|
||||
const videoBlock = VideoBlockSpec()
|
||||
@@ -446,6 +484,7 @@ export const schema = BlockNoteSchema.create({
|
||||
},
|
||||
blockSpecs: {
|
||||
audio: audioBlock,
|
||||
+ guanghuPortalBlock,
|
||||
mathBlock,
|
||||
mermaidBlock,
|
||||
tldrawBlock,
|
||||
diff --git a/src/components/useEditorLinkActivation.test.tsx b/src/components/useEditorLinkActivation.test.tsx
|
||||
index 2234d5c..ab3b62a 100644
|
||||
--- a/src/components/useEditorLinkActivation.test.tsx
|
||||
+++ b/src/components/useEditorLinkActivation.test.tsx
|
||||
@@ -77,22 +77,18 @@ describe('useEditorLinkActivation', () => {
|
||||
mockOpenLocalFile.mockClear()
|
||||
})
|
||||
|
||||
- it('navigates wikilinks only on Cmd+click after the native click stack settles', async () => {
|
||||
+ it('navigates wikilinks on a plain click after the native click stack settles', async () => {
|
||||
const { container, onNavigateWikilink } = renderHarness()
|
||||
const wikilink = appendWikilink(container, 'Alpha Project')
|
||||
|
||||
dispatchMouseEvent(wikilink, 'click')
|
||||
expect(onNavigateWikilink).not.toHaveBeenCalled()
|
||||
|
||||
- const modifiedClick = dispatchMouseEvent(wikilink, 'click', { metaKey: true })
|
||||
- expect(modifiedClick.defaultPrevented).toBe(true)
|
||||
- expect(onNavigateWikilink).not.toHaveBeenCalled()
|
||||
-
|
||||
await Promise.resolve()
|
||||
expect(onNavigateWikilink).toHaveBeenCalledWith('Alpha Project')
|
||||
})
|
||||
|
||||
- it('consumes plain wikilink mousedown and click events before editor internals see stale link nodes', () => {
|
||||
+ it('consumes plain wikilink events before editor internals see stale link nodes', async () => {
|
||||
const { container, onNavigateWikilink } = renderHarness()
|
||||
const wikilink = appendWikilink(container, 'Alpha Project')
|
||||
|
||||
@@ -101,7 +97,8 @@ describe('useEditorLinkActivation', () => {
|
||||
|
||||
expect(mouseDown.defaultPrevented).toBe(true)
|
||||
expect(click.defaultPrevented).toBe(true)
|
||||
- expect(onNavigateWikilink).not.toHaveBeenCalled()
|
||||
+ await Promise.resolve()
|
||||
+ expect(onNavigateWikilink).toHaveBeenCalledWith('Alpha Project')
|
||||
})
|
||||
|
||||
it('blurs an active editor before navigating a Cmd-clicked wikilink', async () => {
|
||||
diff --git a/src/components/useEditorLinkActivation.ts b/src/components/useEditorLinkActivation.ts
|
||||
index aa80834..f29c624 100644
|
||||
--- a/src/components/useEditorLinkActivation.ts
|
||||
+++ b/src/components/useEditorLinkActivation.ts
|
||||
@@ -56,8 +56,6 @@ function activateWikilink(
|
||||
) {
|
||||
consumeEditorLinkEvent(event)
|
||||
|
||||
- if (!hasFollowModifier(event)) return
|
||||
-
|
||||
blurActiveEditable(container)
|
||||
scheduleAfterNativeClick(() => onNavigateWikilink(target))
|
||||
}
|
||||
diff --git a/src/utils/editorDurableMarkdown.ts b/src/utils/editorDurableMarkdown.ts
|
||||
index f0234f1..6e6d30a 100644
|
||||
--- a/src/utils/editorDurableMarkdown.ts
|
||||
+++ b/src/utils/editorDurableMarkdown.ts
|
||||
@@ -15,8 +15,10 @@ import { restoreMarkdownHighlightsInBlocks } from './markdownHighlightMarkdown'
|
||||
import { serializeMathAwareBlocks } from './mathMarkdown'
|
||||
import { mermaidMarkdownCodec } from './mermaidMarkdown'
|
||||
import { tldrawMarkdownCodec } from './tldrawMarkdown'
|
||||
+import { guanghuPortalMarkdownCodec } from './guanghuPortalMarkdown'
|
||||
|
||||
const EDITOR_DURABLE_MARKDOWN_CODECS = [
|
||||
+ guanghuPortalMarkdownCodec,
|
||||
mermaidMarkdownCodec,
|
||||
tldrawMarkdownCodec,
|
||||
] as const
|
||||
diff --git a/src/utils/guanghuPortalMarkdown.test.ts b/src/utils/guanghuPortalMarkdown.test.ts
|
||||
new file mode 100644
|
||||
index 0000000..9bb518a
|
||||
--- /dev/null
|
||||
+++ b/src/utils/guanghuPortalMarkdown.test.ts
|
||||
@@ -0,0 +1,38 @@
|
||||
+import { describe, expect, it } from 'vitest'
|
||||
+import {
|
||||
+ GUANGHU_PORTAL_BLOCK_TYPE,
|
||||
+ guanghuPortalMarkdownCodec,
|
||||
+} from './guanghuPortalMarkdown'
|
||||
+import {
|
||||
+ injectDurableMarkdownBlocks,
|
||||
+ preProcessDurableMarkdownBlocks,
|
||||
+ serializeDurableMarkdownBlocks,
|
||||
+} from './durableMarkdownBlocks'
|
||||
+
|
||||
+const SOURCE = `\`\`\`guanghu-portal
|
||||
+{"title":"铸渊主控台","target":"🌳 铸渊 · Tolaria编号嵌套树 · NAV-CODE-TREE-001","description":"进入现实执行层","status":"运行中"}
|
||||
+\`\`\`
|
||||
+`
|
||||
+
|
||||
+describe('guanghuPortalMarkdownCodec', () => {
|
||||
+ it('round-trips a Guanghu portal card through the rich editor pipeline', () => {
|
||||
+ const processed = preProcessDurableMarkdownBlocks({
|
||||
+ markdown: SOURCE,
|
||||
+ codecs: [guanghuPortalMarkdownCodec],
|
||||
+ })
|
||||
+ const blocks = injectDurableMarkdownBlocks({
|
||||
+ blocks: [{ type: 'paragraph', content: [{ type: 'text', text: processed.trim() }] }],
|
||||
+ codecs: [guanghuPortalMarkdownCodec],
|
||||
+ }) as Array<{ type: string; props: Record<string, string> }>
|
||||
+
|
||||
+ expect(blocks[0]?.type).toBe(GUANGHU_PORTAL_BLOCK_TYPE)
|
||||
+ expect(blocks[0]?.props.title).toBe('铸渊主控台')
|
||||
+ expect(blocks[0]?.props.target).toContain('铸渊')
|
||||
+
|
||||
+ expect(serializeDurableMarkdownBlocks({
|
||||
+ blocks,
|
||||
+ codecs: [guanghuPortalMarkdownCodec],
|
||||
+ serializeOrdinaryBlocks: () => '',
|
||||
+ })).toBe(SOURCE)
|
||||
+ })
|
||||
+})
|
||||
diff --git a/src/utils/guanghuPortalMarkdown.ts b/src/utils/guanghuPortalMarkdown.ts
|
||||
new file mode 100644
|
||||
index 0000000..f083b25
|
||||
--- /dev/null
|
||||
+++ b/src/utils/guanghuPortalMarkdown.ts
|
||||
@@ -0,0 +1,93 @@
|
||||
+import {
|
||||
+ type BlockLike,
|
||||
+ type DurableBlockCodec,
|
||||
+ type DurableFencePayloadInput,
|
||||
+} from './durableMarkdownBlocks'
|
||||
+
|
||||
+export const GUANGHU_PORTAL_BLOCK_TYPE = 'guanghuPortalBlock'
|
||||
+
|
||||
+const TOKEN_PREFIX = '@@TOLARIA_GUANGHU_PORTAL:'
|
||||
+const TOKEN_SUFFIX = '@@'
|
||||
+
|
||||
+export interface GuanghuPortalPayload {
|
||||
+ source: string
|
||||
+ title: string
|
||||
+ target: string
|
||||
+ description: string
|
||||
+ status: string
|
||||
+}
|
||||
+
|
||||
+function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
+ return typeof value === 'object' && value !== null && !Array.isArray(value)
|
||||
+}
|
||||
+
|
||||
+function readString(record: Record<string, unknown>, key: string): string {
|
||||
+ const value = record[key]
|
||||
+ return typeof value === 'string' ? value : ''
|
||||
+}
|
||||
+
|
||||
+function decodePayload(payload: unknown): GuanghuPortalPayload | null {
|
||||
+ if (!isRecord(payload)) return null
|
||||
+ const source = readString(payload, 'source')
|
||||
+ const title = readString(payload, 'title')
|
||||
+ const target = readString(payload, 'target')
|
||||
+ if (!source || !title || !target) return null
|
||||
+ return {
|
||||
+ source,
|
||||
+ title,
|
||||
+ target,
|
||||
+ description: readString(payload, 'description'),
|
||||
+ status: readString(payload, 'status'),
|
||||
+ }
|
||||
+}
|
||||
+
|
||||
+function readFenceMetadata(info: string): Record<string, never> | null {
|
||||
+ return info.trim().toLowerCase() === 'guanghu-portal' ? {} : null
|
||||
+}
|
||||
+
|
||||
+function buildPayload({ lines, start, end }: DurableFencePayloadInput): GuanghuPortalPayload {
|
||||
+ const source = lines.slice(start, end + 1).join('')
|
||||
+ const body = lines.slice(start + 1, end).join('').trim()
|
||||
+ try {
|
||||
+ const decoded = decodePayload({ ...JSON.parse(body), source })
|
||||
+ if (decoded) return decoded
|
||||
+ } catch {
|
||||
+ // Invalid card data remains visible as an ordinary code block.
|
||||
+ }
|
||||
+ return { source, title: '光湖入口', target: '', description: body, status: '配置错误' }
|
||||
+}
|
||||
+
|
||||
+function buildBlock(block: BlockLike, payload: GuanghuPortalPayload): BlockLike {
|
||||
+ if (!payload.target) return block
|
||||
+ return {
|
||||
+ ...block,
|
||||
+ type: GUANGHU_PORTAL_BLOCK_TYPE,
|
||||
+ props: {
|
||||
+ ...(block.props ?? {}),
|
||||
+ source: payload.source,
|
||||
+ title: payload.title,
|
||||
+ target: payload.target,
|
||||
+ description: payload.description,
|
||||
+ status: payload.status,
|
||||
+ },
|
||||
+ content: undefined,
|
||||
+ children: [],
|
||||
+ }
|
||||
+}
|
||||
+
|
||||
+function isPortalBlock(block: BlockLike): boolean {
|
||||
+ return block.type === GUANGHU_PORTAL_BLOCK_TYPE
|
||||
+ && typeof block.props?.source === 'string'
|
||||
+ && typeof block.props?.target === 'string'
|
||||
+}
|
||||
+
|
||||
+export const guanghuPortalMarkdownCodec: DurableBlockCodec = {
|
||||
+ tokenPrefix: TOKEN_PREFIX,
|
||||
+ tokenSuffix: TOKEN_SUFFIX,
|
||||
+ readFenceMetadata,
|
||||
+ buildPayload,
|
||||
+ decodePayload,
|
||||
+ buildBlock: (block, payload) => buildBlock(block, payload as GuanghuPortalPayload),
|
||||
+ isBlock: isPortalBlock,
|
||||
+ serializeBlock: block => block.props?.source ?? '',
|
||||
+}
|
||||
diff --git a/src/utils/wikilink.test.ts b/src/utils/wikilink.test.ts
|
||||
index ed7864e..4aa125e 100644
|
||||
--- a/src/utils/wikilink.test.ts
|
||||
+++ b/src/utils/wikilink.test.ts
|
||||
@@ -273,6 +273,29 @@ describe('canonicalWikilinkTargetForTitle', () => {
|
||||
})
|
||||
})
|
||||
|
||||
+describe('Unicode wikilink resolution', () => {
|
||||
+ it('resolves Chinese and emoji titles across equivalent Unicode forms', () => {
|
||||
+ const entry = makeEntry({
|
||||
+ path: '/vault/第五域/🌳 铸渊 · 导航地图.md',
|
||||
+ filename: '🌳 铸渊 · 导航地图.md',
|
||||
+ title: '🌳 铸渊 · 导航地图',
|
||||
+ })
|
||||
+
|
||||
+ expect(resolveEntry([entry], '🌳 铸渊 · 导航地图')).toBe(entry)
|
||||
+ expect(resolveEntry([entry], '🌳 鑄淵 · 導航地圖')).toBeUndefined()
|
||||
+ })
|
||||
+
|
||||
+ it('resolves an imported file with an id suffix by its heading title', () => {
|
||||
+ const entry = makeEntry({
|
||||
+ path: '/vault/🧭 霜砚 · 总控人格系统 · 母语导航页 b8463f86479348139b5744dd5e995c53.md',
|
||||
+ filename: '🧭 霜砚 · 总控人格系统 · 母语导航页 b8463f86479348139b5744dd5e995c53.md',
|
||||
+ title: '🧭 霜砚 · 总控人格系统 · 母语导航页',
|
||||
+ })
|
||||
+
|
||||
+ expect(resolveEntry([entry], '🧭 霜砚 · 总控人格系统 · 母语导航页')).toBe(entry)
|
||||
+ })
|
||||
+})
|
||||
+
|
||||
describe('formatWikilinkRef', () => {
|
||||
it('wraps a canonical target in wikilink syntax', () => {
|
||||
expect(formatWikilinkRef('projects/alpha')).toBe('[[projects/alpha]]')
|
||||
--
|
||||
2.50.1 (Apple Git-155)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user