dsh-paste-code-block 0.1.2 → 0.2.5

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 CHANGED
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * dsh-paste-code-block — locale dictionaries and label logic (pure, no DOM).
3
3
  *
4
- * Canonical source for the user-visible names of pasted blocks ("复制代码块N"
5
- * vs "Code block #N", etc.). The browser half (`client.js`) embeds its own
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
6
  * copy of this logic because DSH's client module loader resolves *package
7
7
  * names*, not relative ESM files; this module is the single place where that
8
8
  * logic is unit-tested. **Keep `src/i18n.js` and the embedded copy in
@@ -19,8 +19,8 @@ export const NS = 'paste-code-block'
19
19
 
20
20
  /** Simplified Chinese dictionary (the key-set source of truth). */
21
21
  export const zh = {
22
- 'block.code': '复制代码块{n}',
23
- 'block.text': '复制文本块{n}',
22
+ 'block.code': '代码块 {n}',
23
+ 'block.text': '文本块 {n}',
24
24
  'detail.aria': '代码块详情',
25
25
  'slot.label': '粘贴代码块详情',
26
26
  'lines.one': '{count} 行',
@@ -34,12 +34,15 @@ export const zh = {
34
34
  'edit.aria': '编辑代码块内容',
35
35
  'error.stale': '代码块已失效,请重新粘贴',
36
36
  'error.remove': '无法删除该块,请重试',
37
+ 'fold.code': '代码块',
38
+ 'fold.text': '文本块',
39
+ 'fold.aria': '展开或折叠该块',
37
40
  }
38
41
 
39
42
  /** English dictionary, key-identical to the Chinese source of truth. */
40
43
  export const en = {
41
- 'block.code': 'Code block #{n}',
42
- 'block.text': 'Text block #{n}',
44
+ 'block.code': 'Code #{n}',
45
+ 'block.text': 'Text #{n}',
43
46
  'detail.aria': 'Block details',
44
47
  'slot.label': 'Pasted block details',
45
48
  'lines.one': '{count} line',
@@ -53,11 +56,24 @@ export const en = {
53
56
  'edit.aria': 'Edit block content',
54
57
  'error.stale': 'This block is no longer valid — paste it again',
55
58
  'error.remove': 'Could not remove the block — please retry',
59
+ 'fold.code': 'Code',
60
+ 'fold.text': 'Text',
61
+ 'fold.aria': 'Expand or collapse this block',
56
62
  }
57
63
 
58
64
  /** Shipped dictionaries keyed by locale id (mirrors DSH's built-in zh/en set). */
59
65
  export const dictionaries = { zh, en }
60
66
 
67
+ /**
68
+ * Pre-0.1.3 chip labels ("复制代码块N" / "Code block #N" …). No longer
69
+ * rendered, but still recognized by `parseBlockLabel` so chips inserted by an
70
+ * older bundle keep resolving to their block after an in-place page refresh.
71
+ */
72
+ export const legacyLabels = {
73
+ zh: { 'block.code': '复制代码块{n}', 'block.text': '复制文本块{n}' },
74
+ en: { 'block.code': 'Code block #{n}', 'block.text': 'Text block #{n}' },
75
+ }
76
+
61
77
  /**
62
78
  * Standalone translator used only when no DSH locale service is present
63
79
  * (defensive fallback outside the standard web composition). English is the
@@ -82,21 +98,34 @@ export function detectBrowserLocale() {
82
98
  }
83
99
  }
84
100
 
85
- /** Build a block chip label from a translator: 复制代码块2 / Code block #2. */
101
+ /** Build a block chip label from a translator: 代码块 2 / Code #2. */
86
102
  export function blockLabel(t, type, n) {
87
103
  return t(type === 'code' ? 'block.code' : 'block.text', { n })
88
104
  }
89
105
 
90
106
  /**
91
- * Recognize a rendered block label in ANY shipped locale and recover its
92
- * `{ type, n }` identity. This lets a chip click, an ✕ delete, and the
93
- * locale-switch retitling find the right block even when the chip was
94
- * inserted under a different language than the one currently active.
95
- * Returns null for anything that is not a block label.
107
+ * Build the title of a SENT-message fold card: `python · 128 行` /
108
+ * `Code · 128 lines`. `lang` (the fence info string) names code cards when
109
+ * present; text cards and untagged code cards fall back to the localized
110
+ * type name. Line count always renders, so users can gauge size while the
111
+ * card is collapsed.
112
+ */
113
+ export function foldTitle(t, type, lang, lines) {
114
+ const meta = t(lines === 1 ? 'lines.one' : 'lines.other', { count: lines })
115
+ const name = type === 'code' ? (lang || t('fold.code')) : t('fold.text')
116
+ return `${name} · ${meta}`
117
+ }
118
+
119
+ /**
120
+ * Recognize a rendered block label in ANY shipped locale (or a pre-0.1.3
121
+ * legacy locale) and recover its `{ type, n }` identity. This lets a chip
122
+ * click, an ✕ delete, and the locale-switch retitling find the right block
123
+ * even when the chip was inserted under a different language than the one
124
+ * currently active. Returns null for anything that is not a block label.
96
125
  */
97
126
  export function parseBlockLabel(text) {
98
127
  if (!text) return null
99
- for (const dict of [dictionaries.zh, dictionaries.en]) {
128
+ for (const dict of [dictionaries.zh, dictionaries.en, legacyLabels.zh, legacyLabels.en]) {
100
129
  for (const key of ['block.code', 'block.text']) {
101
130
  const pattern = new RegExp(
102
131
  '^' +
package/src/parse.js CHANGED
@@ -99,4 +99,68 @@ export function countMarkdownSignals(text) {
99
99
  export function serializeBlock(block) {
100
100
  const lang = block.isCode ? (block.lang || '') : 'text'
101
101
  return `\n\`\`\`${lang}\n${block.content}\n\`\`\`\n`
102
+ }
103
+
104
+ /**
105
+ * Split a message text into ordered `prose` / `fence` segments using GFM-style
106
+ * fenced code blocks (``` or ~~~, opening fence indented at most 3 spaces,
107
+ * closing fence of >= the opening length and alone on its line).
108
+ *
109
+ * Used by the sent-message fold scanner (see design.md §12): a message the
110
+ * plugin serialized on send comes back as fenced text inside the user
111
+ * bubble, and this function locates the fences so each can be rendered as a
112
+ * collapsed card while the surrounding prose stays visible.
113
+ *
114
+ * Returns [] for empty input. Segments:
115
+ * - { kind: 'prose', text } — verbatim lines between fences
116
+ * - { kind: 'fence', lang, info, body, raw } — complete, terminated fence
117
+ * lang = first word of the info string ('' when absent)
118
+ * info = trimmed info string
119
+ * body = content between the fence lines (no fences, verbatim)
120
+ * raw = the whole slice including both fence lines
121
+ * An unterminated opening fence (no matching close) is kept in the prose run
122
+ * — conservative: we never fold text we are not sure ended.
123
+ */
124
+ export function splitFencedSegments(text) {
125
+ const src = String(text == null ? '' : text)
126
+ if (!src) return []
127
+ const lines = src.split(/\r?\n/)
128
+ const segments = []
129
+ const OPEN = /^ {0,3}(`{3,}|~{3,})(.*)$/
130
+ let proseStart = 0
131
+ let i = 0
132
+ const flushProse = (end) => {
133
+ if (end > proseStart) segments.push({ kind: 'prose', text: lines.slice(proseStart, end).join('\n') })
134
+ }
135
+ while (i < lines.length) {
136
+ const m = OPEN.exec(lines[i])
137
+ if (!m) { i += 1; continue }
138
+ const marker = m[1]
139
+ const ch = marker[0]
140
+ const info = m[2].trim()
141
+ // A ``` fence's info string may not contain backticks (GFM); if it does,
142
+ // this line is ordinary prose, not an opener.
143
+ if (ch === '`' && info.includes('`')) { i += 1; continue }
144
+ const closeRe = new RegExp('^ {0,3}' + (ch === '`' ? '`' : '~') + '{' + marker.length + ',}[ \\t]*$')
145
+ let j = i + 1
146
+ let closed = false
147
+ while (j < lines.length) {
148
+ if (closeRe.test(lines[j])) { closed = true; break }
149
+ j += 1
150
+ }
151
+ if (!closed) { i += 1; continue } // unterminated -> stays prose, keep scanning
152
+ flushProse(i)
153
+ const bodyLines = lines.slice(i + 1, j)
154
+ segments.push({
155
+ kind: 'fence',
156
+ lang: info ? info.split(/\s+/)[0] : '',
157
+ info,
158
+ body: bodyLines.join('\n'),
159
+ raw: lines.slice(i, j + 1).join('\n'),
160
+ })
161
+ i = j + 1
162
+ proseStart = i
163
+ }
164
+ flushProse(lines.length)
165
+ return segments
102
166
  }