dsh-paste-code-block 0.1.1 → 0.1.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/README.md +59 -33
- package/README.zh-CN.md +57 -35
- package/docs/design.md +122 -10
- package/docs/images/hero.png +0 -0
- package/docs/images/hero.svg +4 -0
- package/docs/images/languages.png +0 -0
- package/docs/images/languages.svg +4 -0
- package/docs/images/render.cjs +20 -0
- package/docs/images/workflow.png +0 -0
- package/docs/images/workflow.svg +4 -0
- package/package.json +27 -9
- package/src/chip-nodes.js +46 -0
- package/src/client.js +501 -53
- package/src/i18n.js +121 -0
- package/src/parse.js +3 -2
- package/src/projection.js +54 -0
package/src/i18n.js
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-paste-code-block — locale dictionaries and label logic (pure, no DOM).
|
|
3
|
+
*
|
|
4
|
+
* Canonical source for the user-visible names of pasted blocks ("代码块 1"
|
|
5
|
+
* vs "Code #1", etc.). The browser half (`client.js`) embeds its own
|
|
6
|
+
* copy of this logic because DSH's client module loader resolves *package
|
|
7
|
+
* names*, not relative ESM files; this module is the single place where that
|
|
8
|
+
* logic is unit-tested. **Keep `src/i18n.js` and the embedded copy in
|
|
9
|
+
* `src/client.js` in sync.**
|
|
10
|
+
*
|
|
11
|
+
* Labels render through the DSH locale service (`ctx.locale`), whose
|
|
12
|
+
* `t(key, params)` lookup walks the active language's fallback chain, so the
|
|
13
|
+
* dictionaries below are the whole story: identical key sets in zh and en,
|
|
14
|
+
* `{name}` placeholders for interpolation.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/** Dictionary namespace owned by this plugin. */
|
|
18
|
+
export const NS = 'paste-code-block'
|
|
19
|
+
|
|
20
|
+
/** Simplified Chinese dictionary (the key-set source of truth). */
|
|
21
|
+
export const zh = {
|
|
22
|
+
'block.code': '代码块 {n}',
|
|
23
|
+
'block.text': '文本块 {n}',
|
|
24
|
+
'detail.aria': '代码块详情',
|
|
25
|
+
'slot.label': '粘贴代码块详情',
|
|
26
|
+
'lines.one': '{count} 行',
|
|
27
|
+
'lines.other': '{count} 行',
|
|
28
|
+
'expand': '展开',
|
|
29
|
+
'collapse': '折叠',
|
|
30
|
+
'copy': '复制',
|
|
31
|
+
'copied': '已复制',
|
|
32
|
+
'hide': '收起',
|
|
33
|
+
'remove': '移除',
|
|
34
|
+
'edit.aria': '编辑代码块内容',
|
|
35
|
+
'error.stale': '代码块已失效,请重新粘贴',
|
|
36
|
+
'error.remove': '无法删除该块,请重试',
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** English dictionary, key-identical to the Chinese source of truth. */
|
|
40
|
+
export const en = {
|
|
41
|
+
'block.code': 'Code #{n}',
|
|
42
|
+
'block.text': 'Text #{n}',
|
|
43
|
+
'detail.aria': 'Block details',
|
|
44
|
+
'slot.label': 'Pasted block details',
|
|
45
|
+
'lines.one': '{count} line',
|
|
46
|
+
'lines.other': '{count} lines',
|
|
47
|
+
'expand': 'Expand',
|
|
48
|
+
'collapse': 'Collapse',
|
|
49
|
+
'copy': 'Copy',
|
|
50
|
+
'copied': 'Copied',
|
|
51
|
+
'hide': 'Hide',
|
|
52
|
+
'remove': 'Remove',
|
|
53
|
+
'edit.aria': 'Edit block content',
|
|
54
|
+
'error.stale': 'This block is no longer valid — paste it again',
|
|
55
|
+
'error.remove': 'Could not remove the block — please retry',
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Shipped dictionaries keyed by locale id (mirrors DSH's built-in zh/en set). */
|
|
59
|
+
export const dictionaries = { zh, en }
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Pre-0.1.3 chip labels ("复制代码块N" / "Code block #N" …). No longer
|
|
63
|
+
* rendered, but still recognized by `parseBlockLabel` so chips inserted by an
|
|
64
|
+
* older bundle keep resolving to their block after an in-place page refresh.
|
|
65
|
+
*/
|
|
66
|
+
export const legacyLabels = {
|
|
67
|
+
zh: { 'block.code': '复制代码块{n}', 'block.text': '复制文本块{n}' },
|
|
68
|
+
en: { 'block.code': 'Code block #{n}', 'block.text': 'Text block #{n}' },
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Standalone translator used only when no DSH locale service is present
|
|
73
|
+
* (defensive fallback outside the standard web composition). English is the
|
|
74
|
+
* fallback language, matching DSH's own `FALLBACK_LOCALE`.
|
|
75
|
+
*/
|
|
76
|
+
export function createT(locale) {
|
|
77
|
+
const dict = dictionaries[locale] || dictionaries.en
|
|
78
|
+
return (key, params) => {
|
|
79
|
+
let text = dict[key] != null ? dict[key] : (dictionaries.en[key] != null ? dictionaries.en[key] : key)
|
|
80
|
+
if (params) text = text.replace(/\{(\w+)\}/g, (m, name) => (name in params ? String(params[name]) : m))
|
|
81
|
+
return text
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Guess a locale id from the browser's language list (fallback path only). */
|
|
86
|
+
export function detectBrowserLocale() {
|
|
87
|
+
try {
|
|
88
|
+
const langs = [...(navigator.languages || []), navigator.language].filter(Boolean).map((l) => String(l).toLowerCase())
|
|
89
|
+
return langs.some((l) => l.startsWith('zh')) ? 'zh' : 'en'
|
|
90
|
+
} catch (e) {
|
|
91
|
+
return 'en'
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Build a block chip label from a translator: 代码块 2 / Code #2. */
|
|
96
|
+
export function blockLabel(t, type, n) {
|
|
97
|
+
return t(type === 'code' ? 'block.code' : 'block.text', { n })
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Recognize a rendered block label in ANY shipped locale (or a pre-0.1.3
|
|
102
|
+
* legacy locale) and recover its `{ type, n }` identity. This lets a chip
|
|
103
|
+
* click, an ✕ delete, and the locale-switch retitling find the right block
|
|
104
|
+
* even when the chip was inserted under a different language than the one
|
|
105
|
+
* currently active. Returns null for anything that is not a block label.
|
|
106
|
+
*/
|
|
107
|
+
export function parseBlockLabel(text) {
|
|
108
|
+
if (!text) return null
|
|
109
|
+
for (const dict of [dictionaries.zh, dictionaries.en, legacyLabels.zh, legacyLabels.en]) {
|
|
110
|
+
for (const key of ['block.code', 'block.text']) {
|
|
111
|
+
const pattern = new RegExp(
|
|
112
|
+
'^' +
|
|
113
|
+
dict[key].replace(/[.*+?^${}()|[\]\\]/g, '\\$&').replace(/\\\{n\\\}/g, '(\\d+)') +
|
|
114
|
+
'$',
|
|
115
|
+
)
|
|
116
|
+
const m = pattern.exec(text)
|
|
117
|
+
if (m) return { type: key === 'block.code' ? 'code' : 'text', n: Number(m[1]) }
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return null
|
|
121
|
+
}
|
package/src/parse.js
CHANGED
|
@@ -39,8 +39,9 @@ export function detectLang(text) {
|
|
|
39
39
|
|
|
40
40
|
/**
|
|
41
41
|
* Parse pasted text into a block, or null when it is ordinary short prose
|
|
42
|
-
* that should paste plainly. `isCode` drives both the chip
|
|
43
|
-
*
|
|
42
|
+
* that should paste plainly. `isCode` drives both the chip label (registered
|
|
43
|
+
* per locale via i18n key `block.code` / `block.text` — see `src/i18n.js`)
|
|
44
|
+
* and the fenced language on send.
|
|
44
45
|
*/
|
|
45
46
|
export function parseBlock(raw) {
|
|
46
47
|
const text = String(raw || '').replace(/^\uFEFF/, '')
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Composer-projection geometry.
|
|
3
|
+
*
|
|
4
|
+
* The input shell publishes two coordinate systems for the same document:
|
|
5
|
+
*
|
|
6
|
+
* - `draft` (clipboard projection): every chip expands to its `clipboardText`,
|
|
7
|
+
* so a chip occupies `occurrence.length` characters.
|
|
8
|
+
* - the editor's detect projection (what the scoped edit verbs address): every
|
|
9
|
+
* chip collapses to exactly ONE placeholder character (U+FFFC), while plain
|
|
10
|
+
* text is identical in both.
|
|
11
|
+
*
|
|
12
|
+
* Editing verbs (`slash/input-insert-text`, chip insertion) take detect spans,
|
|
13
|
+
* so clipboard offsets must be converted before addressing an occurrence.
|
|
14
|
+
*
|
|
15
|
+
* Pure and dependency-free so the conversion is unit-testable; keep the embedded
|
|
16
|
+
* copy in src/client.js in sync (same rule as parse.js/i18n.js/chip-nodes.js).
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Convert one occurrence's clipboard offset into a detect offset.
|
|
21
|
+
* @param occurrences - all occurrences of the same projection.
|
|
22
|
+
* @param occ - the occurrence to convert.
|
|
23
|
+
* @returns the detect offset where the occurrence's placeholder sits.
|
|
24
|
+
*/
|
|
25
|
+
export function detectOffsetOf(occurrences, occ) {
|
|
26
|
+
let shift = 0
|
|
27
|
+
for (const other of occurrences || []) {
|
|
28
|
+
if (other === occ || other.offset >= occ.offset) continue
|
|
29
|
+
shift += Math.max(0, (other.length || 1) - 1)
|
|
30
|
+
}
|
|
31
|
+
return occ.offset - shift
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Detect span of the separating space the shell appends right after a freshly
|
|
36
|
+
* inserted chip — or null when the character after that chip is not a space.
|
|
37
|
+
*
|
|
38
|
+
* `SessionInputShell.insertReference` inserts `[chip, ' ']` whenever the
|
|
39
|
+
* character at the pick-time span is not itself a space. Blocks are inserted at
|
|
40
|
+
* the end of the draft, so that character can only have come from the insertion
|
|
41
|
+
* itself: removing it never touches a space the draft already contained.
|
|
42
|
+
*
|
|
43
|
+
* @param projection - published input state (`{ draft, occurrences }`).
|
|
44
|
+
* @param occ - OUR chip, the one that was just inserted.
|
|
45
|
+
* @returns `{ start, end }` in detect coordinates, or null.
|
|
46
|
+
*/
|
|
47
|
+
export function separatorSpaceSpan(projection, occ) {
|
|
48
|
+
if (!projection || !occ) return null
|
|
49
|
+
const draft = projection.draft || ''
|
|
50
|
+
const afterChip = occ.offset + (occ.length || 0)
|
|
51
|
+
if (draft.charAt(afterChip) !== ' ') return null
|
|
52
|
+
const start = detectOffsetOf(projection.occurrences, occ) + 1
|
|
53
|
+
return { start, end: start + 1 }
|
|
54
|
+
}
|