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/README.md +17 -11
- package/README.zh-CN.md +17 -11
- package/docs/design.md +144 -3
- package/docs/images/hero.png +0 -0
- package/docs/images/hero.svg +1 -1
- package/docs/images/languages.png +0 -0
- package/docs/images/languages.svg +1 -1
- package/docs/images/render.cjs +20 -0
- package/docs/images/workflow.png +0 -0
- package/package.json +20 -9
- package/src/client.js +677 -18
- package/src/i18n.js +42 -13
- package/src/parse.js +64 -0
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 ("
|
|
5
|
-
* vs "Code
|
|
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': '
|
|
23
|
-
'block.text': '
|
|
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
|
|
42
|
-
'block.text': 'Text
|
|
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:
|
|
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
|
-
*
|
|
92
|
-
* `
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
*
|
|
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
|
}
|