dsh-quote-annotate-pro 0.3.2 → 0.3.3
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/CHANGELOG.md +11 -0
- package/lib/client.js +79 -25
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
本项目的所有重要变更都记在这里。版本号遵循 [SemVer](https://semver.org/lang/zh-CN/)。
|
|
4
4
|
|
|
5
|
+
## 0.3.3 — 修复「输入框无法显示字」
|
|
6
|
+
|
|
7
|
+
- **根因**:0.3.2 的输入框装饰只**加** class、从不**删**。Lexical 会复用行 DOM:
|
|
8
|
+
用户删掉位置行文字、再从同一个行元素里继续打字时,残留的 `.dshq-cmp-header{display:none}` 会把新打的字一起藏掉
|
|
9
|
+
(用户实测反馈「输入框无法显示字」)
|
|
10
|
+
- **修复**:装饰改为**完整对账**(逐行同时做加类与删类),行元素不再匹配位置行/引用行时立刻恢复可见
|
|
11
|
+
- **光标保护**:光标停在位置行里时,该行临时以半透明显示(`.dshq-cmp-header-active`),移开后再隐藏 —— 编辑位置行也能看见自己打的字
|
|
12
|
+
- **空白兜底**:若整个输入框内容全是位置行且光标就在里面,一律不隐藏(避免输入框看起来是空的)
|
|
13
|
+
- **行识别**:逐层展开含块级子元素的容器(最多 4 层),单行草稿时不会再把整个容器当成一行处理
|
|
14
|
+
- `npm test` 8/8、`node --check`、`check-manifest` 全绿
|
|
15
|
+
|
|
5
16
|
## 0.3.2 — 输入框装饰支持一条消息里多条批注
|
|
6
17
|
|
|
7
18
|
- **修复**:输入框里的「隐藏位置行 + 紧凑引用块」装饰改为**状态机逐行扫描**。此前实现遇到位置行后的第一个空行就 `break`,
|
package/lib/client.js
CHANGED
|
@@ -1165,6 +1165,8 @@ window.__ModuleLoader__.load({
|
|
|
1165
1165
|
.dshq-sent-quote-text{display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical;overflow:hidden;white-space:pre-wrap;word-break:break-word}
|
|
1166
1166
|
/* 输入框里的引用:隐藏位置行,正文显示为紧凑块(输入框与消息观感一致) */
|
|
1167
1167
|
.dshq-cmp-header{display:none !important}
|
|
1168
|
+
/* 光标停在位置行里时临时显示(否则打字看不见字);必须排在上一行之后才能覆盖 !important */
|
|
1169
|
+
.dshq-cmp-header-active{display:block !important;opacity:.55}
|
|
1168
1170
|
.dshq-cmp-quote{background:rgba(127,127,127,.10);border-radius:8px;padding:2px 10px;margin:2px 0}
|
|
1169
1171
|
.dshq-cmp-quote-first{margin-top:4px}
|
|
1170
1172
|
.dshq-cmp-quote-last{margin-bottom:4px}
|
|
@@ -1742,13 +1744,38 @@ window.__ModuleLoader__.load({
|
|
|
1742
1744
|
let observer = null
|
|
1743
1745
|
// 位置行:`【引用 · 第N轮 · DSH 回复】` 或 `【批注 N · 第N轮 · DSH 回复(…)】`
|
|
1744
1746
|
const HEADER_RE = /^【(?:引用|批注)[^\n]*】$/
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1747
|
+
const isBlockEl = (el) => !!el && el.nodeType === 1 && /^(DIV|P|LI)$/.test(el.tagName)
|
|
1748
|
+
/**
|
|
1749
|
+
* 编辑器根下的"行元素":通常是 <div>/<p>;个别宿主会再包一层容器。
|
|
1750
|
+
* 逐层向下展开"含块级子元素"的容器(最多 4 层),叶子层就是真正的行。
|
|
1751
|
+
* (早期版本只在"单容器且子元素 >1"时展开一层,单行草稿时会把整个容器当成一行处理)
|
|
1752
|
+
*/
|
|
1753
|
+
const lineElements = (ed) => {
|
|
1754
|
+
let cur = [ed]
|
|
1755
|
+
for (let depth = 0; depth < 4; depth++) {
|
|
1756
|
+
const next = []
|
|
1757
|
+
let descended = false
|
|
1758
|
+
for (const el of cur) {
|
|
1759
|
+
const blocks = Array.from(el.children || []).filter(isBlockEl)
|
|
1760
|
+
if (blocks.length > 0) { descended = true; for (const b of blocks) next.push(b) } else next.push(el)
|
|
1761
|
+
}
|
|
1762
|
+
if (!descended) return cur
|
|
1763
|
+
cur = next
|
|
1750
1764
|
}
|
|
1751
|
-
return
|
|
1765
|
+
return cur
|
|
1766
|
+
}
|
|
1767
|
+
// 光标所在行:装饰时不能把光标那行藏起来,否则用户打字"看不见字"
|
|
1768
|
+
const caretLineOf = (ed, lines) => {
|
|
1769
|
+
try {
|
|
1770
|
+
const sel = window.getSelection()
|
|
1771
|
+
if (!sel || sel.rangeCount === 0 || !sel.anchorNode) return null
|
|
1772
|
+
let el = sel.anchorNode.nodeType === 1 ? sel.anchorNode : sel.anchorNode.parentElement
|
|
1773
|
+
while (el && el !== ed) {
|
|
1774
|
+
if (lines.indexOf(el) !== -1) return el
|
|
1775
|
+
el = el.parentElement
|
|
1776
|
+
}
|
|
1777
|
+
} catch (e) { /* ignore */ }
|
|
1778
|
+
return null
|
|
1752
1779
|
}
|
|
1753
1780
|
/**
|
|
1754
1781
|
* 把输入框里的正文"装饰"成紧凑块:
|
|
@@ -1756,40 +1783,63 @@ window.__ModuleLoader__.load({
|
|
|
1756
1783
|
* - 位置行后面紧跟的引用正文行 → `.dshq-cmp-quote`(浅色块;首行/末行分别圆角)
|
|
1757
1784
|
* 一条消息里可以有多条批注,所以这里按"状态机"逐行扫描,遇到空行即结束当前块,
|
|
1758
1785
|
* 然后继续扫后面的块(早期版本遇到第一个空行就 break,导致第二条批注不再装饰)。
|
|
1786
|
+
*
|
|
1787
|
+
* 关键:**完整对账**(既加类、也删类)。Lexical 会复用行 DOM,删掉位置行文字后新打的字
|
|
1788
|
+
* 若落回同一个行元素,残留的 `.dshq-cmp-header` 会让新文字看不见(用户反馈"输入框无法显示字")。
|
|
1759
1789
|
*/
|
|
1760
1790
|
const decorate = () => {
|
|
1761
1791
|
try {
|
|
1762
1792
|
for (const ed of document.querySelectorAll('[contenteditable="true"]')) {
|
|
1763
|
-
const
|
|
1793
|
+
const lines = lineElements(ed)
|
|
1794
|
+
const headerSet = new Set()
|
|
1795
|
+
const quoteMap = new Map()
|
|
1764
1796
|
let block = null
|
|
1765
|
-
let changed = 0
|
|
1766
1797
|
const flush = () => {
|
|
1767
|
-
if (block && block.
|
|
1768
|
-
|
|
1769
|
-
lines.forEach((el, i) => {
|
|
1770
|
-
if (!el.classList.contains('dshq-cmp-quote')) changed++
|
|
1771
|
-
el.classList.add('dshq-cmp-quote')
|
|
1772
|
-
el.classList.toggle('dshq-cmp-quote-first', i === 0)
|
|
1773
|
-
el.classList.toggle('dshq-cmp-quote-last', i === lines.length - 1)
|
|
1774
|
-
})
|
|
1798
|
+
if (block && block.length > 0) {
|
|
1799
|
+
block.forEach((el, i) => quoteMap.set(el, { first: i === 0, last: i === block.length - 1 }))
|
|
1775
1800
|
}
|
|
1776
1801
|
block = null
|
|
1777
1802
|
}
|
|
1778
|
-
for (const el of
|
|
1803
|
+
for (const el of lines) {
|
|
1779
1804
|
const t = String(el.textContent || '').trim()
|
|
1780
1805
|
const single = t !== '' && t.indexOf('\n') === -1
|
|
1781
|
-
if (single && HEADER_RE.test(t)) {
|
|
1782
|
-
flush()
|
|
1783
|
-
if (!el.classList.contains('dshq-cmp-header')) changed++
|
|
1784
|
-
el.classList.add('dshq-cmp-header')
|
|
1785
|
-
block = { lines: [] }
|
|
1786
|
-
continue
|
|
1787
|
-
}
|
|
1806
|
+
if (single && HEADER_RE.test(t)) { flush(); headerSet.add(el); block = []; continue }
|
|
1788
1807
|
if (!block) continue
|
|
1789
1808
|
if (t === '' || !single) { flush(); continue }
|
|
1790
|
-
block.
|
|
1809
|
+
block.push(el)
|
|
1791
1810
|
}
|
|
1792
1811
|
flush()
|
|
1812
|
+
// 兜底:如果整段内容都是位置行(用户把正文删光了),并且光标就在里面,
|
|
1813
|
+
// 不隐藏任何行 —— 否则输入框看起来是空的,用户会以为"打不出字"。
|
|
1814
|
+
try {
|
|
1815
|
+
const focused = ed === document.activeElement || ed.contains(document.activeElement)
|
|
1816
|
+
if (focused && lines.length > 0 && headerSet.size === lines.length) headerSet.clear()
|
|
1817
|
+
} catch (e) { /* ignore */ }
|
|
1818
|
+
const caret = caretLineOf(ed, lines)
|
|
1819
|
+
let changed = 0
|
|
1820
|
+
for (const el of lines) {
|
|
1821
|
+
const cls = el.classList
|
|
1822
|
+
const isHeader = headerSet.has(el)
|
|
1823
|
+
const isActive = isHeader && el === caret
|
|
1824
|
+
if (isHeader && !isActive) {
|
|
1825
|
+
if (!cls.contains('dshq-cmp-header')) { cls.add('dshq-cmp-header'); changed++ }
|
|
1826
|
+
} else if (cls.contains('dshq-cmp-header')) { cls.remove('dshq-cmp-header'); changed++ }
|
|
1827
|
+
if (isActive) {
|
|
1828
|
+
if (!cls.contains('dshq-cmp-header-active')) { cls.add('dshq-cmp-header-active'); changed++ }
|
|
1829
|
+
} else if (cls.contains('dshq-cmp-header-active')) { cls.remove('dshq-cmp-header-active'); changed++ }
|
|
1830
|
+
const q = quoteMap.get(el)
|
|
1831
|
+
if (q) {
|
|
1832
|
+
if (!cls.contains('dshq-cmp-quote')) changed++
|
|
1833
|
+
cls.add('dshq-cmp-quote')
|
|
1834
|
+
cls.toggle('dshq-cmp-quote-first', q.first)
|
|
1835
|
+
cls.toggle('dshq-cmp-quote-last', q.last)
|
|
1836
|
+
} else if (cls.contains('dshq-cmp-quote')) {
|
|
1837
|
+
cls.remove('dshq-cmp-quote')
|
|
1838
|
+
cls.remove('dshq-cmp-quote-first')
|
|
1839
|
+
cls.remove('dshq-cmp-quote-last')
|
|
1840
|
+
changed++
|
|
1841
|
+
}
|
|
1842
|
+
}
|
|
1793
1843
|
if (changed > 0) report('cmp:decorated', { changed })
|
|
1794
1844
|
}
|
|
1795
1845
|
} catch (e) { report('cmp:scan-throw', String((e && e.message) || e)) }
|
|
@@ -1803,9 +1853,13 @@ window.__ModuleLoader__.load({
|
|
|
1803
1853
|
observer = new MutationObserver(() => { decorate() })
|
|
1804
1854
|
observer.observe(document.body, { childList: true, subtree: true, characterData: true })
|
|
1805
1855
|
} catch (e) { report('cmp:observer-throw', String((e && e.message) || e)) }
|
|
1856
|
+
// 光标进出位置行时立即更新(光标在位置行里时临时显示该行,避免"打字看不见")
|
|
1857
|
+
const onSelectionChange = () => { try { decorate() } catch (e) { /* ignore */ } }
|
|
1858
|
+
try { document.addEventListener('selectionchange', onSelectionChange) } catch (e) { /* ignore */ }
|
|
1806
1859
|
return () => {
|
|
1807
1860
|
if (composerDecorateNow === decorate) composerDecorateNow = null
|
|
1808
1861
|
if (observer) { try { observer.disconnect() } catch (e) { /* ignore */ } }
|
|
1862
|
+
try { document.removeEventListener('selectionchange', onSelectionChange) } catch (e) { /* ignore */ }
|
|
1809
1863
|
}
|
|
1810
1864
|
}, 'dsh-quote-annotate-pro: composer quote decoration')
|
|
1811
1865
|
} catch (e) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-quote-annotate-pro",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.3",
|
|
4
4
|
"description": "划词批注与引用锚点:划词 → 批注(自动编号)→ 引用;引用块在输入框和已发送消息里始终可见(最多 3 行、位置行隐藏),点一下跳回原文并高亮;模型按「Annotation N:」逐条回应,回复里的标签可悬浮看原文与评语。纯客户端,锚点存本地 localStorage。",
|
|
5
5
|
"keywords": ["dsh", "dsh-plugin", "deepseek-harness", "quote", "annotate", "selection", "reference", "anchor"],
|
|
6
6
|
"license": "MIT",
|