Resync initial event connections and render nested task blocks
This commit is contained in:
1 parent
2df1ab87df
commit
f9cfb138ea
4 files changed
+52
-15
No files matched your search
@@ -0,0 +1,31 @@
|
|||||||
|
// @vitest-environment happy-dom
|
||||||
|
import { afterEach, expect, it, vi } from 'vitest'
|
||||||
|
import { connectEvents } from './events'
|
||||||
|
|
||||||
|
afterEach(() => { vi.useRealTimers(); vi.unstubAllGlobals() })
|
||||||
|
|
||||||
|
it('首次连接和重连均补同步,停止后不再连接', () => {
|
||||||
|
vi.useFakeTimers()
|
||||||
|
class Socket {
|
||||||
|
static CLOSED = 3
|
||||||
|
static instances: Socket[] = []
|
||||||
|
readyState = 0
|
||||||
|
onopen?: () => void
|
||||||
|
onclose?: () => void
|
||||||
|
close() { this.readyState = Socket.CLOSED }
|
||||||
|
constructor() { Socket.instances.push(this) }
|
||||||
|
}
|
||||||
|
vi.stubGlobal('WebSocket', Socket)
|
||||||
|
let synchronized = 0
|
||||||
|
const stop = connectEvents({ onEvents: () => {}, onResync: () => { synchronized++ } })
|
||||||
|
Socket.instances[0].onopen!()
|
||||||
|
expect(synchronized).toBe(1)
|
||||||
|
Socket.instances[0].onclose!()
|
||||||
|
vi.advanceTimersByTime(1000)
|
||||||
|
Socket.instances[1].onopen!()
|
||||||
|
expect(synchronized).toBe(2)
|
||||||
|
stop()
|
||||||
|
Socket.instances[1].onclose!()
|
||||||
|
vi.advanceTimersByTime(30000)
|
||||||
|
expect(Socket.instances).toHaveLength(2)
|
||||||
|
})
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
// 订阅服务端的仓库变更事件;断线后指数退避重连,重连成功时触发全量同步
|
// 订阅服务端的仓库变更事件;断线后指数退避重连,每次连接成功时触发全量同步
|
||||||
|
|
||||||
export interface VaultEvent {
|
export interface VaultEvent {
|
||||||
type: 'tree' | 'changed' | 'deleted' | 'moved' | 'settings' | 'plugins'
|
type: 'tree' | 'changed' | 'deleted' | 'moved' | 'settings' | 'plugins'
|
||||||
@@ -12,16 +12,14 @@ export function connectEvents(handlers: { onEvents: (evs: VaultEvent[]) => void;
|
|||||||
let delay = 1000
|
let delay = 1000
|
||||||
let timer: ReturnType<typeof setTimeout> | undefined
|
let timer: ReturnType<typeof setTimeout> | undefined
|
||||||
let stopped = false
|
let stopped = false
|
||||||
let connectedBefore = false
|
|
||||||
|
|
||||||
const connect = () => {
|
const connect = () => {
|
||||||
const proto = location.protocol === 'https:' ? 'wss:' : 'ws:'
|
const proto = location.protocol === 'https:' ? 'wss:' : 'ws:'
|
||||||
ws = new WebSocket(`${proto}//${location.host}/api/v1/events`)
|
ws = new WebSocket(`${proto}//${location.host}/api/v1/events`)
|
||||||
ws.onopen = () => {
|
ws.onopen = () => {
|
||||||
delay = 1000
|
delay = 1000
|
||||||
// 首次连接前的状态由初始化加载,之后每次重连都可能错过事件
|
// 初始化读取到首次订阅之间也存在空窗,每次建立连接都补同步。
|
||||||
if (connectedBefore) handlers.onResync()
|
handlers.onResync()
|
||||||
connectedBefore = true
|
|
||||||
}
|
}
|
||||||
ws.onmessage = (e) => handlers.onEvents(JSON.parse(e.data))
|
ws.onmessage = (e) => handlers.onEvents(JSON.parse(e.data))
|
||||||
ws.onclose = () => {
|
ws.onclose = () => {
|
||||||
|
|||||||
@@ -65,6 +65,14 @@ describe('安全模式', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe('Typora 扩展', () => {
|
describe('Typora 扩展', () => {
|
||||||
|
it('任务列表保留嵌套列表和后续段落,正确处理松散与紧凑列表', () => {
|
||||||
|
expect(normalize(renderMarkdown('- [ ] 父任务\n - 子任务\n\n 后续说明\n'))).toBe(normalize(
|
||||||
|
'<ul><li><p><input type="checkbox" disabled="" /> 父任务</p><ul><li>子任务</li></ul><p>后续说明</p></li></ul>',
|
||||||
|
))
|
||||||
|
expect(normalize(renderMarkdown('- [x] 已完成\n - [ ] 子任务\n- [ ] 下一项\n'))).toBe(normalize(
|
||||||
|
'<ul><li><input type="checkbox" disabled="" checked="" /> 已完成\n<ul><li><input type="checkbox" disabled="" /> 子任务</li></ul></li><li><input type="checkbox" disabled="" /> 下一项</li></ul>',
|
||||||
|
))
|
||||||
|
})
|
||||||
it('高亮、上下标、脚注引用', () => {
|
it('高亮、上下标、脚注引用', () => {
|
||||||
expect(renderMarkdown('==a== x^2^ H~2~O[^1]')).toBe('<p><mark>a</mark> x<sup>2</sup> H<sub>2</sub>O<sup class="footnote-ref"><a href="#fn-1">1</a></sup></p>\n')
|
expect(renderMarkdown('==a== x^2^ H~2~O[^1]')).toBe('<p><mark>a</mark> x<sup>2</sup> H<sub>2</sub>O<sup class="footnote-ref"><a href="#fn-1">1</a></sup></p>\n')
|
||||||
})
|
})
|
||||||
|
|||||||
+10
-10
@@ -362,7 +362,7 @@ export function renderMarkdown(src: string, { parser = markdownParser, unsafe =
|
|||||||
function blockChildren(node: SyntaxNode, withDefinitions = false): SyntaxNode[] {
|
function blockChildren(node: SyntaxNode, withDefinitions = false): SyntaxNode[] {
|
||||||
const out: SyntaxNode[] = []
|
const out: SyntaxNode[] = []
|
||||||
for (let ch = node.firstChild; ch; ch = ch.nextSibling) {
|
for (let ch = node.firstChild; ch; ch = ch.nextSibling) {
|
||||||
if (/Mark$/.test(ch.name) || ch.name === 'Task') continue
|
if (/Mark$/.test(ch.name)) continue
|
||||||
if (ch.name !== 'LinkReference' || withDefinitions) out.push(ch)
|
if (ch.name !== 'LinkReference' || withDefinitions) out.push(ch)
|
||||||
}
|
}
|
||||||
return out
|
return out
|
||||||
@@ -403,6 +403,12 @@ export function renderMarkdown(src: string, { parser = markdownParser, unsafe =
|
|||||||
const body = inline(node, node.from, node.to).trim()
|
const body = inline(node, node.from, node.to).trim()
|
||||||
return tight ? body + '\n' : `<p>${body}</p>\n`
|
return tight ? body + '\n' : `<p>${body}</p>\n`
|
||||||
}
|
}
|
||||||
|
case 'Task': {
|
||||||
|
const marker = node.getChild('TaskMarker')!
|
||||||
|
const checked = text(marker.from + 1, marker.from + 2) !== ' ' ? ' checked=""' : ''
|
||||||
|
const body = `<input type="checkbox" disabled=""${checked} /> ${inline(node, marker.to, node.to).trim()}`
|
||||||
|
return tight ? body + '\n' : `<p>${body}</p>\n`
|
||||||
|
}
|
||||||
case 'Blockquote':
|
case 'Blockquote':
|
||||||
return `<blockquote>\n${blocks(node)}</blockquote>\n`
|
return `<blockquote>\n${blocks(node)}</blockquote>\n`
|
||||||
case 'BulletList':
|
case 'BulletList':
|
||||||
@@ -467,18 +473,12 @@ export function renderMarkdown(src: string, { parser = markdownParser, unsafe =
|
|||||||
|
|
||||||
function listItem(item: SyntaxNode, tight: boolean): string {
|
function listItem(item: SyntaxNode, tight: boolean): string {
|
||||||
const kids = blockChildren(item)
|
const kids = blockChildren(item)
|
||||||
const task = item.getChild('Task')
|
|
||||||
if (task) {
|
|
||||||
const marker = task.getChild('TaskMarker')!
|
|
||||||
const checked = text(marker.from + 1, marker.from + 2) !== ' ' ? ' checked=""' : ''
|
|
||||||
const body = inline(task, marker.to, task.to).trim()
|
|
||||||
return `<li><input type="checkbox" disabled=""${checked} /> ${body}</li>\n`
|
|
||||||
}
|
|
||||||
if (!kids.length) return '<li></li>\n'
|
if (!kids.length) return '<li></li>\n'
|
||||||
let body = ''
|
let body = ''
|
||||||
for (const k of kids) body += block(k, tight)
|
for (const k of kids) body += block(k, tight)
|
||||||
const startsInline = tight && kids[0].name === 'Paragraph'
|
const startsInline = tight && (kids[0].name === 'Paragraph' || kids[0].name === 'Task')
|
||||||
const endsInline = tight && kids[kids.length - 1].name === 'Paragraph'
|
const last = kids[kids.length - 1].name
|
||||||
|
const endsInline = tight && (last === 'Paragraph' || last === 'Task')
|
||||||
if (!startsInline) body = '\n' + body
|
if (!startsInline) body = '\n' + body
|
||||||
if (endsInline) body = body.replace(/\n$/, '')
|
if (endsInline) body = body.replace(/\n$/, '')
|
||||||
return `<li>${body}</li>\n`
|
return `<li>${body}</li>\n`
|
||||||
|
|||||||
Reference in new issue
Block a user