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/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 wording
43
- * ("复制代码块N" vs "复制文本块N") and the fenced language on send.
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
+ }