@tiwater/office-mcp 0.21.29 → 0.21.30
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/office/contracts/tiwater-office-provider-contract-manifest-v1.json +1 -1
- package/package.json +10 -4
- package/pdf/contracts/tiwater-pdf-provider-contract-manifest-v1.json +1 -1
- package/text/README.md +14 -0
- package/text/contracts/text_inspect.schema.json +24 -0
- package/text/contracts/text_read_lines.schema.json +36 -0
- package/text/contracts/tiwater-text-provider-contract-manifest-v1.json +31 -0
- package/text/index.mjs +180 -0
- package/text/observation.mjs +162 -0
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tiwater/office-mcp",
|
|
3
|
-
"version": "0.21.
|
|
4
|
-
"description": "Published MCP distribution for independent Tiwater Office and
|
|
3
|
+
"version": "0.21.30",
|
|
4
|
+
"description": "Published MCP distribution for independent Tiwater Office, PDF, and Text document capabilities",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"repository": {
|
|
@@ -11,7 +11,8 @@
|
|
|
11
11
|
},
|
|
12
12
|
"bin": {
|
|
13
13
|
"tiwater-office-mcp": "office/index.mjs",
|
|
14
|
-
"tiwater-pdf-mcp": "pdf/index.mjs"
|
|
14
|
+
"tiwater-pdf-mcp": "pdf/index.mjs",
|
|
15
|
+
"tiwater-text-mcp": "text/index.mjs"
|
|
15
16
|
},
|
|
16
17
|
"engines": {
|
|
17
18
|
"node": ">=20"
|
|
@@ -28,7 +29,12 @@
|
|
|
28
29
|
"office/contracts/*.schema.json",
|
|
29
30
|
"pdf/index.mjs",
|
|
30
31
|
"pdf/README.md",
|
|
31
|
-
"pdf/contracts/*.json"
|
|
32
|
+
"pdf/contracts/*.json",
|
|
33
|
+
"text/index.mjs",
|
|
34
|
+
"text/observation.mjs",
|
|
35
|
+
"text/README.md",
|
|
36
|
+
"text/contracts/tiwater-text-provider-contract-manifest-v1.json",
|
|
37
|
+
"text/contracts/*.schema.json"
|
|
32
38
|
],
|
|
33
39
|
"publishConfig": {
|
|
34
40
|
"access": "public"
|
package/text/README.md
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# tiwater-text-mcp
|
|
2
|
+
|
|
3
|
+
`tiwater-text-mcp` publishes read-only observation for explicitly supported
|
|
4
|
+
plain-text files. `text_inspect` reports the exact byte identity, lossless
|
|
5
|
+
decoding facts, line count, and bounded opening lines. `text_read_lines` reads
|
|
6
|
+
one explicit zero-based line page and reports its continuation.
|
|
7
|
+
|
|
8
|
+
Supported inputs are `.txt`, `.text`, `.log`, `.csv`, `.tsv`, `.md`, and
|
|
9
|
+
`.markdown` files encoded as valid UTF-8, UTF-8 with BOM, UTF-16LE with BOM, or
|
|
10
|
+
UTF-16BE with BOM. The provider rejects binary content and never guesses an
|
|
11
|
+
encoding.
|
|
12
|
+
|
|
13
|
+
The tools do not interpret key-value pairs, records, sections, Markdown, or
|
|
14
|
+
business fields, and never modify or transcode the source.
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "tiwater.text-mcp-input/text_inspect/v1",
|
|
4
|
+
"type": "object",
|
|
5
|
+
"properties": {
|
|
6
|
+
"input": {
|
|
7
|
+
"type": "string",
|
|
8
|
+
"minLength": 1,
|
|
9
|
+
"x-tiwater-file-role": "read"
|
|
10
|
+
},
|
|
11
|
+
"returnContent": {
|
|
12
|
+
"type": "boolean",
|
|
13
|
+
"description": "Return the complete bounded inspection directly."
|
|
14
|
+
},
|
|
15
|
+
"output": {
|
|
16
|
+
"type": "string",
|
|
17
|
+
"minLength": 1,
|
|
18
|
+
"description": "New immutable JSON artifact path for the complete inspection.",
|
|
19
|
+
"x-tiwater-file-role": "write"
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
"required": ["input", "returnContent", "output"],
|
|
23
|
+
"additionalProperties": false
|
|
24
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "tiwater.text-mcp-input/text_read_lines/v1",
|
|
4
|
+
"type": "object",
|
|
5
|
+
"properties": {
|
|
6
|
+
"input": {
|
|
7
|
+
"type": "string",
|
|
8
|
+
"minLength": 1,
|
|
9
|
+
"x-tiwater-file-role": "read"
|
|
10
|
+
},
|
|
11
|
+
"offset": {
|
|
12
|
+
"type": "integer",
|
|
13
|
+
"minimum": 0,
|
|
14
|
+
"maximum": 9007199254740991,
|
|
15
|
+
"description": "Zero-based line offset in the exact current source revision."
|
|
16
|
+
},
|
|
17
|
+
"limit": {
|
|
18
|
+
"type": "integer",
|
|
19
|
+
"minimum": 1,
|
|
20
|
+
"maximum": 200,
|
|
21
|
+
"description": "Maximum number of lines in this bounded page."
|
|
22
|
+
},
|
|
23
|
+
"returnContent": {
|
|
24
|
+
"type": "boolean",
|
|
25
|
+
"description": "Return this selected line page directly when it fits the response limit. May be combined with output."
|
|
26
|
+
},
|
|
27
|
+
"output": {
|
|
28
|
+
"type": "string",
|
|
29
|
+
"minLength": 1,
|
|
30
|
+
"description": "Optional immutable JSON artifact path for this selected line page. May be combined with returnContent.",
|
|
31
|
+
"x-tiwater-file-role": "write"
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
"required": ["input", "offset", "limit", "returnContent"],
|
|
35
|
+
"additionalProperties": false
|
|
36
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schema": "tiwater.text-provider-contract-manifest/v1",
|
|
3
|
+
"provider": {
|
|
4
|
+
"id": "@tiwater/office-mcp",
|
|
5
|
+
"version": "0.21.30"
|
|
6
|
+
},
|
|
7
|
+
"tools": [
|
|
8
|
+
{
|
|
9
|
+
"name": "text_inspect",
|
|
10
|
+
"providerContract": {
|
|
11
|
+
"source": "servers/text/provider-contracts/text_inspect.schema.json",
|
|
12
|
+
"sha256": "41d463771e4cf5d0c9377ba70a139ca043ca9542f074c7d6425f0b711cfa50b8"
|
|
13
|
+
},
|
|
14
|
+
"inputContract": {
|
|
15
|
+
"path": "text/contracts/text_inspect.schema.json",
|
|
16
|
+
"sha256": "41d463771e4cf5d0c9377ba70a139ca043ca9542f074c7d6425f0b711cfa50b8"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
"name": "text_read_lines",
|
|
21
|
+
"providerContract": {
|
|
22
|
+
"source": "servers/text/provider-contracts/text_read_lines.schema.json",
|
|
23
|
+
"sha256": "0f9202f0b45bbd35b3ba4bebec7776da41f2c8668c7cdece068c44102f518bb6"
|
|
24
|
+
},
|
|
25
|
+
"inputContract": {
|
|
26
|
+
"path": "text/contracts/text_read_lines.schema.json",
|
|
27
|
+
"sha256": "0f9202f0b45bbd35b3ba4bebec7776da41f2c8668c7cdece068c44102f518bb6"
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
]
|
|
31
|
+
}
|
package/text/index.mjs
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { createHash } from 'node:crypto';
|
|
4
|
+
import { readFile } from 'node:fs/promises';
|
|
5
|
+
import * as z from 'zod/v4';
|
|
6
|
+
import { McpServer } from '@modelcontextprotocol/server';
|
|
7
|
+
import { serveStdio } from '@modelcontextprotocol/server/stdio';
|
|
8
|
+
|
|
9
|
+
import { createToolResult } from '../_shared/tool-runtime.mjs';
|
|
10
|
+
import { deliverLargeJsonResult } from '../_shared/large-json-result.mjs';
|
|
11
|
+
import { withOutputWriteLock } from '../_shared/output-write-lock.mjs';
|
|
12
|
+
import { inspectText, readTextLines } from './observation.mjs';
|
|
13
|
+
|
|
14
|
+
const packageMetadata = JSON.parse(await readFile(new URL('../package.json', import.meta.url), 'utf8'));
|
|
15
|
+
const contractManifest = JSON.parse(await readFile(
|
|
16
|
+
new URL('./contracts/tiwater-text-provider-contract-manifest-v1.json', import.meta.url),
|
|
17
|
+
'utf8',
|
|
18
|
+
));
|
|
19
|
+
if (contractManifest.schema !== 'tiwater.text-provider-contract-manifest/v1'
|
|
20
|
+
|| contractManifest.provider?.id !== packageMetadata.name
|
|
21
|
+
|| contractManifest.provider?.version !== packageMetadata.version) {
|
|
22
|
+
throw new Error('Text MCP input contract manifest does not match the installed distribution');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const inputContracts = new Map(await Promise.all(contractManifest.tools.map(async entry => {
|
|
26
|
+
const bytes = await readFile(new URL(`./contracts/${entry.name}.schema.json`, import.meta.url));
|
|
27
|
+
const hash = createHash('sha256').update(bytes).digest('hex');
|
|
28
|
+
if (hash !== entry.inputContract.sha256) {
|
|
29
|
+
throw new Error(`Text MCP input contract hash mismatch: ${entry.name}`);
|
|
30
|
+
}
|
|
31
|
+
return [entry.name, z.fromJSONSchema(JSON.parse(bytes.toString('utf8')))];
|
|
32
|
+
})));
|
|
33
|
+
|
|
34
|
+
const openingLineLimit = 8;
|
|
35
|
+
const artifact = z.object({
|
|
36
|
+
path: z.string(),
|
|
37
|
+
sha256: z.string().regex(/^[0-9a-f]{64}$/),
|
|
38
|
+
bytes: z.number().int().nonnegative(),
|
|
39
|
+
}).strict();
|
|
40
|
+
const runtimeIdentity = z.object({ command: z.literal('tiwater-text-mcp'), cwd: z.string() }).strict();
|
|
41
|
+
const decoding = z.object({
|
|
42
|
+
status: z.literal('lossless'),
|
|
43
|
+
encoding: z.enum(['utf-8', 'utf-16le', 'utf-16be']),
|
|
44
|
+
bom: z.enum(['none', 'utf-8', 'utf-16le', 'utf-16be']),
|
|
45
|
+
}).strict();
|
|
46
|
+
const lineIdentity = z.object({
|
|
47
|
+
sourceSha256: z.string().regex(/^[0-9a-f]{64}$/),
|
|
48
|
+
index: z.number().int().nonnegative(),
|
|
49
|
+
}).strict();
|
|
50
|
+
const terminator = z.enum(['none', 'lf', 'crlf', 'cr']);
|
|
51
|
+
const openingLine = z.object({
|
|
52
|
+
identity: lineIdentity,
|
|
53
|
+
textPreview: z.string(),
|
|
54
|
+
textLength: z.number().int().nonnegative(),
|
|
55
|
+
terminator,
|
|
56
|
+
}).strict();
|
|
57
|
+
const inspectIdentity = z.object({
|
|
58
|
+
source: artifact,
|
|
59
|
+
extension: z.string(),
|
|
60
|
+
decoding,
|
|
61
|
+
lineCount: z.number().int().nonnegative(),
|
|
62
|
+
openingLines: z.array(openingLine).max(openingLineLimit),
|
|
63
|
+
}).strict();
|
|
64
|
+
const linePageReceipt = z.object({
|
|
65
|
+
schema: z.literal('tiwater.text-line-page-receipt/v1'),
|
|
66
|
+
totalLineCount: z.number().int().nonnegative(),
|
|
67
|
+
returnedLineCount: z.number().int().nonnegative(),
|
|
68
|
+
remaining: z.number().int().nonnegative(),
|
|
69
|
+
nextOffset: z.number().int().nonnegative().nullable(),
|
|
70
|
+
}).strict();
|
|
71
|
+
const textLine = z.object({ identity: lineIdentity, text: z.string(), terminator }).strict();
|
|
72
|
+
const inspectContent = z.object({
|
|
73
|
+
schema: z.literal('tiwater.text-inspection/v1'),
|
|
74
|
+
source: artifact,
|
|
75
|
+
extension: z.string(),
|
|
76
|
+
decoding,
|
|
77
|
+
lineCount: z.number().int().nonnegative(),
|
|
78
|
+
openingLines: z.array(openingLine).max(openingLineLimit),
|
|
79
|
+
}).strict();
|
|
80
|
+
const linePage = z.object({
|
|
81
|
+
schema: z.literal('tiwater.text-line-page/v1'),
|
|
82
|
+
source: artifact,
|
|
83
|
+
extension: z.string(),
|
|
84
|
+
decoding,
|
|
85
|
+
receipt: linePageReceipt,
|
|
86
|
+
lines: z.array(textLine).max(200),
|
|
87
|
+
}).strict();
|
|
88
|
+
|
|
89
|
+
function largeResultOutput(contentSchema) {
|
|
90
|
+
return z.object({
|
|
91
|
+
tool: z.string(),
|
|
92
|
+
runtime: runtimeIdentity,
|
|
93
|
+
sources: z.array(artifact).min(1).max(1),
|
|
94
|
+
returnContent: z.boolean(),
|
|
95
|
+
artifact: artifact.nullable(),
|
|
96
|
+
receipt: z.object({
|
|
97
|
+
contentBytes: z.number().int().nonnegative(),
|
|
98
|
+
contentReturned: z.boolean(),
|
|
99
|
+
contentWritten: z.boolean(),
|
|
100
|
+
}).strict(),
|
|
101
|
+
content: contentSchema.optional(),
|
|
102
|
+
}).strict();
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const definitions = [
|
|
106
|
+
{
|
|
107
|
+
name: 'text_inspect',
|
|
108
|
+
description: 'Inspect one exact supported plain-text revision. Return its byte identity, lossless encoding and BOM facts, line count, and at most eight opening line identities while retaining the complete bounded inspection at output. It does not parse fields, records, key-value pairs, sections, or markup.',
|
|
109
|
+
outputSchema: largeResultOutput(inspectContent).extend({ identity: inspectIdentity }).strict(),
|
|
110
|
+
handler: textInspect,
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
name: 'text_read_lines',
|
|
114
|
+
description: 'Read one explicit zero-based line page from one exact supported plain-text revision. The receipt reports remaining lines and nextOffset; continue only when another line is needed. Set returnContent true to return the selected page when it fits the response limit. Provide output to retain the same complete page. These channels are independent and may be used together; at least one is required. Lines retain their exact decoded text and terminator; the provider does not interpret fields, records, key-value pairs, sections, Markdown, or business meaning.',
|
|
115
|
+
outputSchema: largeResultOutput(linePage).extend({ summary: linePageReceipt }).strict(),
|
|
116
|
+
handler: textReadLines,
|
|
117
|
+
},
|
|
118
|
+
];
|
|
119
|
+
|
|
120
|
+
function buildServer() {
|
|
121
|
+
const server = new McpServer(
|
|
122
|
+
{ name: 'tiwater-text', version: packageMetadata.version },
|
|
123
|
+
{ instructions: 'Observe only exact supported plain-text bytes and explicit zero-based line pages. A read-only output path is an immutable artifact identity: an identical request may replay it; every different request uses a different path. Callers own all interpretation and business meaning.' },
|
|
124
|
+
);
|
|
125
|
+
for (const definition of definitions) {
|
|
126
|
+
const inputSchema = inputContracts.get(definition.name);
|
|
127
|
+
if (!inputSchema) throw new Error(`Missing provider-owned Text MCP input contract: ${definition.name}`);
|
|
128
|
+
server.registerTool(
|
|
129
|
+
definition.name,
|
|
130
|
+
{
|
|
131
|
+
description: definition.description,
|
|
132
|
+
inputSchema,
|
|
133
|
+
outputSchema: definition.outputSchema,
|
|
134
|
+
annotations: {
|
|
135
|
+
readOnlyHint: true,
|
|
136
|
+
idempotentHint: true,
|
|
137
|
+
destructiveHint: false,
|
|
138
|
+
openWorldHint: false,
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
async args => {
|
|
142
|
+
const payload = typeof args.output === 'string'
|
|
143
|
+
? await withOutputWriteLock(args.output, () => definition.handler(args))
|
|
144
|
+
: await definition.handler(args);
|
|
145
|
+
return createToolResult(payload);
|
|
146
|
+
},
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
return server;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async function textInspect(args) {
|
|
153
|
+
const observation = await inspectText(args.input);
|
|
154
|
+
const delivered = await deliverLargeJsonResult({
|
|
155
|
+
tool: 'text_inspect',
|
|
156
|
+
args,
|
|
157
|
+
runtime: runtime(),
|
|
158
|
+
payload: observation.payload,
|
|
159
|
+
sourcePaths: [observation.input],
|
|
160
|
+
});
|
|
161
|
+
return { ...delivered, identity: observation.identity };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
async function textReadLines(args) {
|
|
165
|
+
const observation = await readTextLines(args.input, args.offset, args.limit);
|
|
166
|
+
const delivered = await deliverLargeJsonResult({
|
|
167
|
+
tool: 'text_read_lines',
|
|
168
|
+
args,
|
|
169
|
+
runtime: runtime(),
|
|
170
|
+
payload: observation.payload,
|
|
171
|
+
sourcePaths: [observation.input],
|
|
172
|
+
});
|
|
173
|
+
return { ...delivered, summary: observation.receipt };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function runtime() {
|
|
177
|
+
return { command: 'tiwater-text-mcp', cwd: process.cwd() };
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
serveStdio(buildServer);
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
import { fileArtifact } from '../_shared/large-json-result.mjs';
|
|
5
|
+
|
|
6
|
+
const supportedExtensions = new Set(['.txt', '.text', '.log', '.csv', '.tsv', '.md', '.markdown']);
|
|
7
|
+
const openingLineLimit = 8;
|
|
8
|
+
const openingTextLimit = 160;
|
|
9
|
+
|
|
10
|
+
export async function inspectText(inputValue) {
|
|
11
|
+
const observation = await observeText(inputValue);
|
|
12
|
+
const source = await fileArtifact(observation.input);
|
|
13
|
+
const openingLines = observation.lines.slice(0, openingLineLimit).map(line => ({
|
|
14
|
+
identity: { sourceSha256: source.sha256, index: line.index },
|
|
15
|
+
textPreview: preview(line.text, openingTextLimit),
|
|
16
|
+
textLength: [...line.text].length,
|
|
17
|
+
terminator: line.terminator,
|
|
18
|
+
}));
|
|
19
|
+
const identity = {
|
|
20
|
+
source,
|
|
21
|
+
extension: observation.extension,
|
|
22
|
+
decoding: observation.decoding,
|
|
23
|
+
lineCount: observation.lines.length,
|
|
24
|
+
openingLines,
|
|
25
|
+
};
|
|
26
|
+
return {
|
|
27
|
+
input: observation.input,
|
|
28
|
+
identity,
|
|
29
|
+
payload: { schema: 'tiwater.text-inspection/v1', ...identity },
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export async function readTextLines(inputValue, requestedOffset, requestedLimit) {
|
|
34
|
+
if (!Number.isSafeInteger(requestedOffset) || requestedOffset < 0) {
|
|
35
|
+
throw Object.assign(new Error('offset must be a non-negative safe integer'), { code: -32602 });
|
|
36
|
+
}
|
|
37
|
+
if (!Number.isSafeInteger(requestedLimit) || requestedLimit < 1 || requestedLimit > 200) {
|
|
38
|
+
throw Object.assign(new Error('limit must be a safe integer from 1 through 200'), { code: -32602 });
|
|
39
|
+
}
|
|
40
|
+
const observation = await observeText(inputValue);
|
|
41
|
+
const source = await fileArtifact(observation.input);
|
|
42
|
+
const offset = Math.min(requestedOffset, observation.lines.length);
|
|
43
|
+
const selected = observation.lines.slice(offset, offset + requestedLimit);
|
|
44
|
+
const nextOffset = offset + selected.length < observation.lines.length
|
|
45
|
+
? offset + selected.length
|
|
46
|
+
: null;
|
|
47
|
+
const receipt = {
|
|
48
|
+
schema: 'tiwater.text-line-page-receipt/v1',
|
|
49
|
+
totalLineCount: observation.lines.length,
|
|
50
|
+
returnedLineCount: selected.length,
|
|
51
|
+
remaining: observation.lines.length - offset - selected.length,
|
|
52
|
+
nextOffset,
|
|
53
|
+
};
|
|
54
|
+
return {
|
|
55
|
+
input: observation.input,
|
|
56
|
+
receipt,
|
|
57
|
+
payload: {
|
|
58
|
+
schema: 'tiwater.text-line-page/v1',
|
|
59
|
+
source,
|
|
60
|
+
extension: observation.extension,
|
|
61
|
+
decoding: observation.decoding,
|
|
62
|
+
receipt,
|
|
63
|
+
lines: selected.map(line => ({
|
|
64
|
+
identity: { sourceSha256: source.sha256, index: line.index },
|
|
65
|
+
text: line.text,
|
|
66
|
+
terminator: line.terminator,
|
|
67
|
+
})),
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export async function observeText(inputValue) {
|
|
73
|
+
if (typeof inputValue !== 'string' || inputValue.trim() === '') {
|
|
74
|
+
throw Object.assign(new Error('input must be a non-empty string'), { code: -32602 });
|
|
75
|
+
}
|
|
76
|
+
const input = path.resolve(inputValue);
|
|
77
|
+
const extension = path.extname(input).toLowerCase();
|
|
78
|
+
if (!supportedExtensions.has(extension)) {
|
|
79
|
+
throw Object.assign(new Error(`unsupported-plain-text-extension:${extension || '(none)'}`), { code: -32602 });
|
|
80
|
+
}
|
|
81
|
+
const bytes = await readFile(input);
|
|
82
|
+
const decoded = decodeLosslessly(bytes);
|
|
83
|
+
rejectBinaryControls(decoded.text);
|
|
84
|
+
return { input, extension, decoding: decoded.decoding, lines: splitLines(decoded.text) };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function decodeLosslessly(bytes) {
|
|
88
|
+
if (bytes.subarray(0, 3).equals(Buffer.from([0xef, 0xbb, 0xbf]))) {
|
|
89
|
+
return decodeWithRoundTrip(bytes.subarray(3), 'utf-8', 'utf-8');
|
|
90
|
+
}
|
|
91
|
+
if (bytes.subarray(0, 2).equals(Buffer.from([0xff, 0xfe]))) {
|
|
92
|
+
return decodeWithRoundTrip(bytes.subarray(2), 'utf-16le', 'utf-16le');
|
|
93
|
+
}
|
|
94
|
+
if (bytes.subarray(0, 2).equals(Buffer.from([0xfe, 0xff]))) {
|
|
95
|
+
return decodeWithRoundTrip(bytes.subarray(2), 'utf-16be', 'utf-16be');
|
|
96
|
+
}
|
|
97
|
+
return decodeWithRoundTrip(bytes, 'utf-8', 'none');
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function decodeWithRoundTrip(bytes, encoding, bom) {
|
|
101
|
+
if ((encoding === 'utf-16le' || encoding === 'utf-16be') && bytes.length % 2 !== 0) {
|
|
102
|
+
throw Object.assign(new Error(`invalid-${encoding}-byte-length`), { code: -32602 });
|
|
103
|
+
}
|
|
104
|
+
let text;
|
|
105
|
+
try {
|
|
106
|
+
text = new TextDecoder(encoding, { fatal: true }).decode(bytes);
|
|
107
|
+
} catch {
|
|
108
|
+
throw Object.assign(new Error(`invalid-${encoding}-sequence`), { code: -32602 });
|
|
109
|
+
}
|
|
110
|
+
let encoded = encoding === 'utf-8' ? Buffer.from(text, 'utf8') : Buffer.from(text, 'utf16le');
|
|
111
|
+
if (encoding === 'utf-16be') encoded = swapUtf16Bytes(encoded);
|
|
112
|
+
if (!encoded.equals(bytes)) {
|
|
113
|
+
throw Object.assign(new Error(`non-lossless-${encoding}-decode`), { code: -32602 });
|
|
114
|
+
}
|
|
115
|
+
return { text, decoding: { status: 'lossless', encoding, bom } };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function swapUtf16Bytes(bytes) {
|
|
119
|
+
const swapped = Buffer.allocUnsafe(bytes.length);
|
|
120
|
+
for (let index = 0; index < bytes.length; index += 2) {
|
|
121
|
+
swapped[index] = bytes[index + 1];
|
|
122
|
+
swapped[index + 1] = bytes[index];
|
|
123
|
+
}
|
|
124
|
+
return swapped;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function rejectBinaryControls(text) {
|
|
128
|
+
const binary = [...text].some(character => {
|
|
129
|
+
const code = character.codePointAt(0);
|
|
130
|
+
return code <= 8 || code === 11 || code === 12 || (code >= 14 && code <= 31)
|
|
131
|
+
|| (code >= 127 && code <= 159);
|
|
132
|
+
});
|
|
133
|
+
if (binary) throw Object.assign(new Error('binary-control-content-is-not-plain-text'), { code: -32602 });
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function splitLines(text) {
|
|
137
|
+
if (text.length === 0) return [];
|
|
138
|
+
const lines = [];
|
|
139
|
+
let start = 0;
|
|
140
|
+
while (start < text.length) {
|
|
141
|
+
let end = start;
|
|
142
|
+
while (end < text.length && text[end] !== '\r' && text[end] !== '\n') end++;
|
|
143
|
+
let terminator = 'none';
|
|
144
|
+
let next = end;
|
|
145
|
+
if (end < text.length) {
|
|
146
|
+
if (text[end] === '\r' && text[end + 1] === '\n') {
|
|
147
|
+
terminator = 'crlf';
|
|
148
|
+
next = end + 2;
|
|
149
|
+
} else {
|
|
150
|
+
terminator = text[end] === '\r' ? 'cr' : 'lf';
|
|
151
|
+
next = end + 1;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
lines.push({ index: lines.length, text: text.slice(start, end), terminator });
|
|
155
|
+
start = next;
|
|
156
|
+
}
|
|
157
|
+
return lines;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function preview(text, limit) {
|
|
161
|
+
return [...text].slice(0, limit).join('');
|
|
162
|
+
}
|