dsh-cait 1.0.3 → 1.0.4

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/lib/xlsx.js CHANGED
@@ -1,218 +1,218 @@
1
- import { readZipEntries, writeZip } from './docx.js'
2
- import { CatError } from './errors.js'
3
- import { decodeXml, escapeXml } from './xml.js'
4
-
5
- export const XLSX_MIME = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
6
-
7
- const SOURCE_HEADERS = new Set(['source', 'src', 'original', '原文', '源文', '源文本'])
8
- const TARGET_HEADERS = new Set(['target', 'tgt', 'translation', '译文', '目标', '目标文本'])
9
-
10
- function fail(code, message) {
11
- return new CatError(code, message)
12
- }
13
-
14
- function zipText(entries, name) {
15
- const entry = entries.find((item) => item.name === name)
16
- return entry ? entry.data.toString('utf8') : ''
17
- }
18
-
19
- function attributes(raw) {
20
- const attrs = {}
21
- for (const match of String(raw).matchAll(/([\w:.-]+)\s*=\s*"([^"]*)"/g)) {
22
- attrs[match[1]] = decodeXml(match[2])
23
- }
24
- return attrs
25
- }
26
-
27
- function sharedStringTable(xml) {
28
- if (!xml) return []
29
- return [...xml.matchAll(/<si\b[^>]*>([\s\S]*?)<\/si>/gi)].map((match) => (
30
- [...match[1].matchAll(/<t\b[^>]*>([\s\S]*?)<\/t>/gi)]
31
- .map((text) => decodeXml(text[1]))
32
- .join('')
33
- ))
34
- }
35
-
36
- function firstSheetPath(entries) {
37
- const workbook = zipText(entries, 'xl/workbook.xml')
38
- const rels = zipText(entries, 'xl/_rels/workbook.xml.rels')
39
- if (workbook && rels) {
40
- const sheet = workbook.match(/<sheet\b([^>]*)\/?>/i)
41
- const id = sheet ? attributes(sheet[1])['r:id'] : ''
42
- if (id) {
43
- const relation = [...rels.matchAll(/<Relationship\b([^>]*)\/?>/gi)]
44
- .map((match) => attributes(match[1]))
45
- .find((item) => item.Id === id)
46
- if (relation?.Target) {
47
- const target = relation.Target.replace(/^\/+/, '')
48
- return target.startsWith('xl/') ? target : `xl/${target}`
49
- }
50
- }
51
- }
52
- const fallback = entries.find((item) => /^xl\/worksheets\/sheet\d+\.xml$/i.test(item.name))
53
- return fallback?.name ?? 'xl/worksheets/sheet1.xml'
54
- }
55
-
56
- function columnIndex(reference) {
57
- const letters = String(reference).match(/^[A-Z]+/i)?.[0]?.toUpperCase() ?? ''
58
- let value = 0
59
- for (const letter of letters) value = value * 26 + (letter.charCodeAt(0) - 64)
60
- return value - 1
61
- }
62
-
63
- function cellValue(attrs, body, shared) {
64
- const type = attrs.t || ''
65
- const inline = body.match(/<is\b[^>]*>([\s\S]*?)<\/is>/i)
66
- if (type === 'inlineStr' || inline) {
67
- const source = inline ? inline[1] : body
68
- return [...source.matchAll(/<t\b[^>]*>([\s\S]*?)<\/t>/gi)]
69
- .map((match) => decodeXml(match[1]))
70
- .join('')
71
- }
72
- const value = body.match(/<v\b[^>]*>([\s\S]*?)<\/v>/i)
73
- if (!value) return ''
74
- if (type === 's') return shared[Number(value[1])] ?? ''
75
- if (type === 'b') return value[1] === '1' ? 'TRUE' : 'FALSE'
76
- return decodeXml(value[1])
77
- }
78
-
79
- function parseRows(xml, shared) {
80
- const rows = new Map()
81
- for (const rowMatch of String(xml).matchAll(/<row\b([^>]*)>([\s\S]*?)<\/row>/gi)) {
82
- const rowAttrs = attributes(rowMatch[1])
83
- const rowNumber = Number(rowAttrs.r) || rows.size + 1
84
- const cells = new Map()
85
- for (const cellMatch of rowMatch[2].matchAll(/<c\b([^>]*?)(?:\/>|>([\s\S]*?)<\/c>)/gi)) {
86
- const attrs = attributes(cellMatch[1])
87
- if (!attrs.r) continue
88
- cells.set(columnIndex(attrs.r), cellValue(attrs, cellMatch[2] ?? '', shared))
89
- }
90
- if (cells.size) rows.set(rowNumber, cells)
91
- }
92
- return [...rows.entries()].sort((left, right) => left[0] - right[0])
93
- }
94
-
95
- export function parseXlsx(buffer) {
96
- let entries
97
- try {
98
- entries = readZipEntries(buffer)
99
- } catch (error) {
100
- throw fail('XLSX_PARSE', `XLSX 文件不是有效的 ZIP/Excel 包: ${error.message}`)
101
- }
102
- const shared = sharedStringTable(zipText(entries, 'xl/sharedStrings.xml'))
103
- const sheetPath = firstSheetPath(entries)
104
- const sheetXml = zipText(entries, sheetPath)
105
- if (!sheetXml) throw fail('XLSX_PARSE', 'XLSX 中找不到工作表。')
106
- const rows = parseRows(sheetXml, shared)
107
- if (!rows.length) throw fail('XLSX_NO_UNITS', 'XLSX 中没有可用的行。')
108
-
109
- const maxColumns = rows.reduce((max, [, cells]) => Math.max(max, ...cells.keys(), -1), -1) + 1
110
- const first = rows[0][1]
111
- const header = (index) => String(first.get(index) ?? '').trim().toLowerCase()
112
- let sourceIndex = -1
113
- let targetIndex = -1
114
- for (let index = 0; index < maxColumns; index += 1) {
115
- if (SOURCE_HEADERS.has(header(index))) sourceIndex = index
116
- if (TARGET_HEADERS.has(header(index))) targetIndex = index
117
- }
118
- const hasHeader = sourceIndex >= 0 && targetIndex >= 0
119
- if (!hasHeader) {
120
- sourceIndex = 0
121
- targetIndex = maxColumns > 1 ? 1 : -1
122
- }
123
-
124
- const segments = []
125
- for (const [rowNumber, cells] of rows) {
126
- if (hasHeader && rowNumber === rows[0][0]) continue
127
- const source = String(cells.get(sourceIndex) ?? '').trim()
128
- if (!source) continue
129
- const target = targetIndex >= 0 ? String(cells.get(targetIndex) ?? '').trim() : ''
130
- segments.push({
131
- source,
132
- target,
133
- paragraph: segments.length,
134
- meta: { format: 'xlsx', xlsxRow: rowNumber },
135
- })
136
- }
137
- if (!segments.length) throw fail('XLSX_NO_UNITS', 'XLSX 中没有可用的源文行。')
138
- return { format: 'xlsx', segments, warnings: [] }
139
- }
140
-
141
- function inlineCell(reference, value) {
142
- return `<c r="${reference}" t="inlineStr"><is><t xml:space="preserve">${escapeXml(value)}</t></is></c>`
143
- }
144
-
145
- function columnName(index) {
146
- let value = index + 1
147
- let name = ''
148
- while (value > 0) {
149
- const remainder = (value - 1) % 26
150
- name = String.fromCharCode(65 + remainder) + name
151
- value = Math.floor((value - 1) / 26)
152
- }
153
- return name
154
- }
155
-
156
- export function buildXlsx(doc) {
157
- const rows = [[
158
- inlineCell('A1', 'Source'),
159
- inlineCell('B1', 'Target'),
160
- ]]
161
- doc.segments.forEach((segment, index) => {
162
- const row = index + 2
163
- rows.push([
164
- inlineCell(`A${row}`, segment.source || ''),
165
- inlineCell(`B${row}`, segment.target || ''),
166
- ])
167
- })
168
- const sheetData = rows
169
- .map((cells, index) => ` <row r="${index + 1}">${cells.join('')}</row>`)
170
- .join('\n')
171
- const lastRow = rows.length + 1
172
- const sheet = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
173
- <worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">
174
- <dimension ref="A1:B${lastRow}"/>
175
- <sheetViews><sheetView workbookViewId="0"/></sheetViews>
176
- <sheetFormatPr defaultRowHeight="15"/>
177
- <sheetData>
178
- ${sheetData}
179
- </sheetData>
180
- </worksheet>`
181
- const contentTypes = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
182
- <Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">
183
- <Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>
184
- <Default Extension="xml" ContentType="application/xml"/>
185
- <Override PartName="/xl/workbook.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml"/>
186
- <Override PartName="/xl/worksheets/sheet1.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"/>
187
- <Override PartName="/xl/styles.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml"/>
188
- </Types>`
189
- const rootRels = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
190
- <Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
191
- <Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="xl/workbook.xml"/>
192
- </Relationships>`
193
- const workbook = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
194
- <workbook xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
195
- <sheets><sheet name="CAIT" sheetId="1" r:id="rId1"/></sheets>
196
- </workbook>`
197
- const workbookRels = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
198
- <Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
199
- <Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet" Target="worksheets/sheet1.xml"/>
200
- <Relationship Id="rId2" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles" Target="styles.xml"/>
201
- </Relationships>`
202
- const styles = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
203
- <styleSheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">
204
- <fonts count="1"><font><sz val="11"/><name val="Calibri"/></font></fonts>
205
- <fills count="1"><fill><patternFill patternType="none"/></fill></fills>
206
- <borders count="1"><border/></borders>
207
- <cellStyleXfs count="1"><xf numFmtId="0" fontId="0" fillId="0" borderId="0"/></cellStyleXfs>
208
- <cellXfs count="1"><xf numFmtId="0" fontId="0" fillId="0" borderId="0" xfId="0"/></cellXfs>
209
- </styleSheet>`
210
- return writeZip([
211
- { name: '[Content_Types].xml', data: Buffer.from(contentTypes, 'utf8') },
212
- { name: '_rels/.rels', data: Buffer.from(rootRels, 'utf8') },
213
- { name: 'xl/workbook.xml', data: Buffer.from(workbook, 'utf8') },
214
- { name: 'xl/_rels/workbook.xml.rels', data: Buffer.from(workbookRels, 'utf8') },
215
- { name: 'xl/worksheets/sheet1.xml', data: Buffer.from(sheet, 'utf8') },
216
- { name: 'xl/styles.xml', data: Buffer.from(styles, 'utf8') },
217
- ])
218
- }
1
+ import { readZipEntries, writeZip } from './docx.js'
2
+ import { CatError } from './errors.js'
3
+ import { decodeXml, escapeXml } from './xml.js'
4
+
5
+ export const XLSX_MIME = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
6
+
7
+ const SOURCE_HEADERS = new Set(['source', 'src', 'original', '原文', '源文', '源文本'])
8
+ const TARGET_HEADERS = new Set(['target', 'tgt', 'translation', '译文', '目标', '目标文本'])
9
+
10
+ function fail(code, message) {
11
+ return new CatError(code, message)
12
+ }
13
+
14
+ function zipText(entries, name) {
15
+ const entry = entries.find((item) => item.name === name)
16
+ return entry ? entry.data.toString('utf8') : ''
17
+ }
18
+
19
+ function attributes(raw) {
20
+ const attrs = {}
21
+ for (const match of String(raw).matchAll(/([\w:.-]+)\s*=\s*"([^"]*)"/g)) {
22
+ attrs[match[1]] = decodeXml(match[2])
23
+ }
24
+ return attrs
25
+ }
26
+
27
+ function sharedStringTable(xml) {
28
+ if (!xml) return []
29
+ return [...xml.matchAll(/<si\b[^>]*>([\s\S]*?)<\/si>/gi)].map((match) => (
30
+ [...match[1].matchAll(/<t\b[^>]*>([\s\S]*?)<\/t>/gi)]
31
+ .map((text) => decodeXml(text[1]))
32
+ .join('')
33
+ ))
34
+ }
35
+
36
+ function firstSheetPath(entries) {
37
+ const workbook = zipText(entries, 'xl/workbook.xml')
38
+ const rels = zipText(entries, 'xl/_rels/workbook.xml.rels')
39
+ if (workbook && rels) {
40
+ const sheet = workbook.match(/<sheet\b([^>]*)\/?>/i)
41
+ const id = sheet ? attributes(sheet[1])['r:id'] : ''
42
+ if (id) {
43
+ const relation = [...rels.matchAll(/<Relationship\b([^>]*)\/?>/gi)]
44
+ .map((match) => attributes(match[1]))
45
+ .find((item) => item.Id === id)
46
+ if (relation?.Target) {
47
+ const target = relation.Target.replace(/^\/+/, '')
48
+ return target.startsWith('xl/') ? target : `xl/${target}`
49
+ }
50
+ }
51
+ }
52
+ const fallback = entries.find((item) => /^xl\/worksheets\/sheet\d+\.xml$/i.test(item.name))
53
+ return fallback?.name ?? 'xl/worksheets/sheet1.xml'
54
+ }
55
+
56
+ function columnIndex(reference) {
57
+ const letters = String(reference).match(/^[A-Z]+/i)?.[0]?.toUpperCase() ?? ''
58
+ let value = 0
59
+ for (const letter of letters) value = value * 26 + (letter.charCodeAt(0) - 64)
60
+ return value - 1
61
+ }
62
+
63
+ function cellValue(attrs, body, shared) {
64
+ const type = attrs.t || ''
65
+ const inline = body.match(/<is\b[^>]*>([\s\S]*?)<\/is>/i)
66
+ if (type === 'inlineStr' || inline) {
67
+ const source = inline ? inline[1] : body
68
+ return [...source.matchAll(/<t\b[^>]*>([\s\S]*?)<\/t>/gi)]
69
+ .map((match) => decodeXml(match[1]))
70
+ .join('')
71
+ }
72
+ const value = body.match(/<v\b[^>]*>([\s\S]*?)<\/v>/i)
73
+ if (!value) return ''
74
+ if (type === 's') return shared[Number(value[1])] ?? ''
75
+ if (type === 'b') return value[1] === '1' ? 'TRUE' : 'FALSE'
76
+ return decodeXml(value[1])
77
+ }
78
+
79
+ function parseRows(xml, shared) {
80
+ const rows = new Map()
81
+ for (const rowMatch of String(xml).matchAll(/<row\b([^>]*)>([\s\S]*?)<\/row>/gi)) {
82
+ const rowAttrs = attributes(rowMatch[1])
83
+ const rowNumber = Number(rowAttrs.r) || rows.size + 1
84
+ const cells = new Map()
85
+ for (const cellMatch of rowMatch[2].matchAll(/<c\b([^>]*?)(?:\/>|>([\s\S]*?)<\/c>)/gi)) {
86
+ const attrs = attributes(cellMatch[1])
87
+ if (!attrs.r) continue
88
+ cells.set(columnIndex(attrs.r), cellValue(attrs, cellMatch[2] ?? '', shared))
89
+ }
90
+ if (cells.size) rows.set(rowNumber, cells)
91
+ }
92
+ return [...rows.entries()].sort((left, right) => left[0] - right[0])
93
+ }
94
+
95
+ export function parseXlsx(buffer) {
96
+ let entries
97
+ try {
98
+ entries = readZipEntries(buffer)
99
+ } catch (error) {
100
+ throw fail('XLSX_PARSE', `XLSX 文件不是有效的 ZIP/Excel 包: ${error.message}`)
101
+ }
102
+ const shared = sharedStringTable(zipText(entries, 'xl/sharedStrings.xml'))
103
+ const sheetPath = firstSheetPath(entries)
104
+ const sheetXml = zipText(entries, sheetPath)
105
+ if (!sheetXml) throw fail('XLSX_PARSE', 'XLSX 中找不到工作表。')
106
+ const rows = parseRows(sheetXml, shared)
107
+ if (!rows.length) throw fail('XLSX_NO_UNITS', 'XLSX 中没有可用的行。')
108
+
109
+ const maxColumns = rows.reduce((max, [, cells]) => Math.max(max, ...cells.keys(), -1), -1) + 1
110
+ const first = rows[0][1]
111
+ const header = (index) => String(first.get(index) ?? '').trim().toLowerCase()
112
+ let sourceIndex = -1
113
+ let targetIndex = -1
114
+ for (let index = 0; index < maxColumns; index += 1) {
115
+ if (SOURCE_HEADERS.has(header(index))) sourceIndex = index
116
+ if (TARGET_HEADERS.has(header(index))) targetIndex = index
117
+ }
118
+ const hasHeader = sourceIndex >= 0 && targetIndex >= 0
119
+ if (!hasHeader) {
120
+ sourceIndex = 0
121
+ targetIndex = maxColumns > 1 ? 1 : -1
122
+ }
123
+
124
+ const segments = []
125
+ for (const [rowNumber, cells] of rows) {
126
+ if (hasHeader && rowNumber === rows[0][0]) continue
127
+ const source = String(cells.get(sourceIndex) ?? '').trim()
128
+ if (!source) continue
129
+ const target = targetIndex >= 0 ? String(cells.get(targetIndex) ?? '').trim() : ''
130
+ segments.push({
131
+ source,
132
+ target,
133
+ paragraph: segments.length,
134
+ meta: { format: 'xlsx', xlsxRow: rowNumber },
135
+ })
136
+ }
137
+ if (!segments.length) throw fail('XLSX_NO_UNITS', 'XLSX 中没有可用的源文行。')
138
+ return { format: 'xlsx', segments, warnings: [] }
139
+ }
140
+
141
+ function inlineCell(reference, value) {
142
+ return `<c r="${reference}" t="inlineStr"><is><t xml:space="preserve">${escapeXml(value)}</t></is></c>`
143
+ }
144
+
145
+ function columnName(index) {
146
+ let value = index + 1
147
+ let name = ''
148
+ while (value > 0) {
149
+ const remainder = (value - 1) % 26
150
+ name = String.fromCharCode(65 + remainder) + name
151
+ value = Math.floor((value - 1) / 26)
152
+ }
153
+ return name
154
+ }
155
+
156
+ export function buildXlsx(doc) {
157
+ const rows = [[
158
+ inlineCell('A1', 'Source'),
159
+ inlineCell('B1', 'Target'),
160
+ ]]
161
+ doc.segments.forEach((segment, index) => {
162
+ const row = index + 2
163
+ rows.push([
164
+ inlineCell(`A${row}`, segment.source || ''),
165
+ inlineCell(`B${row}`, segment.target || ''),
166
+ ])
167
+ })
168
+ const sheetData = rows
169
+ .map((cells, index) => ` <row r="${index + 1}">${cells.join('')}</row>`)
170
+ .join('\n')
171
+ const lastRow = rows.length + 1
172
+ const sheet = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
173
+ <worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">
174
+ <dimension ref="A1:B${lastRow}"/>
175
+ <sheetViews><sheetView workbookViewId="0"/></sheetViews>
176
+ <sheetFormatPr defaultRowHeight="15"/>
177
+ <sheetData>
178
+ ${sheetData}
179
+ </sheetData>
180
+ </worksheet>`
181
+ const contentTypes = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
182
+ <Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">
183
+ <Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>
184
+ <Default Extension="xml" ContentType="application/xml"/>
185
+ <Override PartName="/xl/workbook.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml"/>
186
+ <Override PartName="/xl/worksheets/sheet1.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"/>
187
+ <Override PartName="/xl/styles.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml"/>
188
+ </Types>`
189
+ const rootRels = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
190
+ <Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
191
+ <Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="xl/workbook.xml"/>
192
+ </Relationships>`
193
+ const workbook = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
194
+ <workbook xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
195
+ <sheets><sheet name="CAIT" sheetId="1" r:id="rId1"/></sheets>
196
+ </workbook>`
197
+ const workbookRels = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
198
+ <Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
199
+ <Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet" Target="worksheets/sheet1.xml"/>
200
+ <Relationship Id="rId2" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles" Target="styles.xml"/>
201
+ </Relationships>`
202
+ const styles = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
203
+ <styleSheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">
204
+ <fonts count="1"><font><sz val="11"/><name val="Calibri"/></font></fonts>
205
+ <fills count="1"><fill><patternFill patternType="none"/></fill></fills>
206
+ <borders count="1"><border/></borders>
207
+ <cellStyleXfs count="1"><xf numFmtId="0" fontId="0" fillId="0" borderId="0"/></cellStyleXfs>
208
+ <cellXfs count="1"><xf numFmtId="0" fontId="0" fillId="0" borderId="0" xfId="0"/></cellXfs>
209
+ </styleSheet>`
210
+ return writeZip([
211
+ { name: '[Content_Types].xml', data: Buffer.from(contentTypes, 'utf8') },
212
+ { name: '_rels/.rels', data: Buffer.from(rootRels, 'utf8') },
213
+ { name: 'xl/workbook.xml', data: Buffer.from(workbook, 'utf8') },
214
+ { name: 'xl/_rels/workbook.xml.rels', data: Buffer.from(workbookRels, 'utf8') },
215
+ { name: 'xl/worksheets/sheet1.xml', data: Buffer.from(sheet, 'utf8') },
216
+ { name: 'xl/styles.xml', data: Buffer.from(styles, 'utf8') },
217
+ ])
218
+ }
package/lib/xml.js CHANGED
@@ -1,31 +1,31 @@
1
- /** Minimal, dependency-free XML helpers for TMX and DOCX. */
2
-
3
- const NAMED = {
4
- amp: '&',
5
- lt: '<',
6
- gt: '>',
7
- quot: '"',
8
- apos: "'",
9
- nbsp: '\u00a0',
10
- }
11
-
12
- export function decodeXml(value) {
13
- return String(value ?? '').replace(/&(#x[0-9a-f]+|#\d+|[a-z]+);/gi, (all, body) => {
14
- if (body[0] === '#') {
15
- const code = body[1].toLowerCase() === 'x'
16
- ? Number.parseInt(body.slice(2), 16)
17
- : Number.parseInt(body.slice(1), 10)
18
- return Number.isFinite(code) ? String.fromCodePoint(code) : all
19
- }
20
- return NAMED[body.toLowerCase()] ?? all
21
- })
22
- }
23
-
24
- export function escapeXml(value) {
25
- return String(value ?? '')
26
- .replace(/&/g, '&amp;')
27
- .replace(/</g, '&lt;')
28
- .replace(/>/g, '&gt;')
29
- .replace(/"/g, '&quot;')
30
- .replace(/'/g, '&apos;')
31
- }
1
+ /** Minimal, dependency-free XML helpers for TMX and DOCX. */
2
+
3
+ const NAMED = {
4
+ amp: '&',
5
+ lt: '<',
6
+ gt: '>',
7
+ quot: '"',
8
+ apos: "'",
9
+ nbsp: '\u00a0',
10
+ }
11
+
12
+ export function decodeXml(value) {
13
+ return String(value ?? '').replace(/&(#x[0-9a-f]+|#\d+|[a-z]+);/gi, (all, body) => {
14
+ if (body[0] === '#') {
15
+ const code = body[1].toLowerCase() === 'x'
16
+ ? Number.parseInt(body.slice(2), 16)
17
+ : Number.parseInt(body.slice(1), 10)
18
+ return Number.isFinite(code) ? String.fromCodePoint(code) : all
19
+ }
20
+ return NAMED[body.toLowerCase()] ?? all
21
+ })
22
+ }
23
+
24
+ export function escapeXml(value) {
25
+ return String(value ?? '')
26
+ .replace(/&/g, '&amp;')
27
+ .replace(/</g, '&lt;')
28
+ .replace(/>/g, '&gt;')
29
+ .replace(/"/g, '&quot;')
30
+ .replace(/'/g, '&apos;')
31
+ }
package/locale/en.json CHANGED
@@ -1,11 +1,11 @@
1
- {
2
- "meta": {
3
- "title": "CAIT (dsh-cait)",
4
- "description": "A CAT (computer-aided translation) workbench inside DSH: translation memory, glossary, segment editing, AI translation suggestions, and TXT/Markdown/DOCX/TMX import and export."
5
- },
6
- "match.exact": "Exact",
7
- "match.near_exact": "Near exact",
8
- "match.high_fuzzy": "High fuzzy",
9
- "match.medium_fuzzy": "Medium fuzzy",
10
- "match.low_fuzzy": "Low fuzzy"
11
- }
1
+ {
2
+ "meta": {
3
+ "title": "CAIT (dsh-cait)",
4
+ "description": "A CAT (computer-aided translation) workbench inside DSH: translation memory, glossary, segment editing, AI translation suggestions, and TXT/Markdown/DOCX/TMX import and export."
5
+ },
6
+ "match.exact": "Exact",
7
+ "match.near_exact": "Near exact",
8
+ "match.high_fuzzy": "High fuzzy",
9
+ "match.medium_fuzzy": "Medium fuzzy",
10
+ "match.low_fuzzy": "Low fuzzy"
11
+ }
package/locale/zh.json CHANGED
@@ -1,11 +1,11 @@
1
- {
2
- "meta": {
3
- "title": "CAIT 翻译工作台 (dsh-cait)",
4
- "description": "DSH 内置的 CAT(计算机辅助翻译)工作台:翻译记忆、术语库、句段编辑、AI 翻译建议,以及 TXT/Markdown/DOCX/TMX 导入导出。"
5
- },
6
- "match.exact": "精确",
7
- "match.near_exact": "近似精确",
8
- "match.high_fuzzy": "高模糊",
9
- "match.medium_fuzzy": "中模糊",
10
- "match.low_fuzzy": "低模糊"
11
- }
1
+ {
2
+ "meta": {
3
+ "title": "CAIT 翻译工作台 (dsh-cait)",
4
+ "description": "DSH 内置的 CAT(计算机辅助翻译)工作台:翻译记忆、术语库、句段编辑、AI 翻译建议,以及 TXT/Markdown/DOCX/TMX 导入导出。"
5
+ },
6
+ "match.exact": "精确",
7
+ "match.near_exact": "近似精确",
8
+ "match.high_fuzzy": "高模糊",
9
+ "match.medium_fuzzy": "中模糊",
10
+ "match.low_fuzzy": "低模糊"
11
+ }