robloxstudio-mcp 2.5.0-next.0 → 2.5.0
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/dist/install-plugin-cli.js +87 -0
- package/package.json +3 -2
- package/studio-plugin/MCPPlugin.rbxmx +78 -11
- package/studio-plugin/src/modules/handlers/CaptureHandlers.ts +24 -4
- package/studio-plugin/src/modules/handlers/QueryHandlers.ts +25 -8
- package/studio-plugin/src/modules/handlers/ScriptHandlers.ts +1 -0
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/install-plugin.ts
|
|
4
|
+
import { createWriteStream, existsSync, mkdirSync, unlinkSync } from "fs";
|
|
5
|
+
import { join } from "path";
|
|
6
|
+
import { homedir } from "os";
|
|
7
|
+
import { get } from "https";
|
|
8
|
+
var REPO = "boshyxd/robloxstudio-mcp";
|
|
9
|
+
var ASSET_NAME = "MCPPlugin.rbxmx";
|
|
10
|
+
var TIMEOUT_MS = 3e4;
|
|
11
|
+
var MAX_REDIRECTS = 5;
|
|
12
|
+
function getPluginsFolder() {
|
|
13
|
+
if (process.platform === "win32") {
|
|
14
|
+
return join(process.env.LOCALAPPDATA || join(homedir(), "AppData", "Local"), "Roblox", "Plugins");
|
|
15
|
+
}
|
|
16
|
+
return join(homedir(), "Documents", "Roblox", "Plugins");
|
|
17
|
+
}
|
|
18
|
+
function httpsGet(url) {
|
|
19
|
+
return new Promise((resolve, reject) => {
|
|
20
|
+
const req = get(url, { headers: { "User-Agent": "robloxstudio-mcp" } }, resolve);
|
|
21
|
+
req.on("error", reject);
|
|
22
|
+
req.setTimeout(TIMEOUT_MS, () => {
|
|
23
|
+
req.destroy(new Error(`Request timed out after ${TIMEOUT_MS}ms`));
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
async function download(url, dest, redirects = 0) {
|
|
28
|
+
const res = await httpsGet(url);
|
|
29
|
+
if (res.statusCode === 301 || res.statusCode === 302) {
|
|
30
|
+
if (redirects >= MAX_REDIRECTS) throw new Error(`Too many redirects (max ${MAX_REDIRECTS})`);
|
|
31
|
+
const location = res.headers.location;
|
|
32
|
+
if (!location) throw new Error("Redirect with no location header");
|
|
33
|
+
return download(location, dest, redirects + 1);
|
|
34
|
+
}
|
|
35
|
+
if (res.statusCode !== 200) {
|
|
36
|
+
throw new Error(`Download failed: HTTP ${res.statusCode}`);
|
|
37
|
+
}
|
|
38
|
+
return new Promise((resolve, reject) => {
|
|
39
|
+
const file = createWriteStream(dest);
|
|
40
|
+
const cleanup = (err) => {
|
|
41
|
+
file.close(() => {
|
|
42
|
+
try {
|
|
43
|
+
unlinkSync(dest);
|
|
44
|
+
} catch {
|
|
45
|
+
}
|
|
46
|
+
reject(err);
|
|
47
|
+
});
|
|
48
|
+
};
|
|
49
|
+
res.pipe(file);
|
|
50
|
+
file.on("finish", () => {
|
|
51
|
+
file.close();
|
|
52
|
+
resolve();
|
|
53
|
+
});
|
|
54
|
+
file.on("error", cleanup);
|
|
55
|
+
res.on("error", cleanup);
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
async function installPlugin() {
|
|
59
|
+
const pluginsFolder = getPluginsFolder();
|
|
60
|
+
if (!existsSync(pluginsFolder)) {
|
|
61
|
+
mkdirSync(pluginsFolder, { recursive: true });
|
|
62
|
+
}
|
|
63
|
+
console.log("Fetching latest release...");
|
|
64
|
+
const res = await httpsGet(`https://api.github.com/repos/${REPO}/releases/latest`);
|
|
65
|
+
if (res.statusCode !== 200) {
|
|
66
|
+
throw new Error(`GitHub API returned HTTP ${res.statusCode}`);
|
|
67
|
+
}
|
|
68
|
+
const chunks = [];
|
|
69
|
+
for await (const chunk of res) {
|
|
70
|
+
chunks.push(chunk);
|
|
71
|
+
}
|
|
72
|
+
const release = JSON.parse(Buffer.concat(chunks).toString());
|
|
73
|
+
const asset = release.assets?.find((a) => a.name === ASSET_NAME);
|
|
74
|
+
if (!asset) {
|
|
75
|
+
throw new Error(`${ASSET_NAME} not found in release ${release.tag_name}`);
|
|
76
|
+
}
|
|
77
|
+
const dest = join(pluginsFolder, ASSET_NAME);
|
|
78
|
+
console.log(`Downloading ${ASSET_NAME} from ${release.tag_name}...`);
|
|
79
|
+
await download(asset.browser_download_url, dest);
|
|
80
|
+
console.log(`Installed to ${dest}`);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// src/install-plugin-cli.ts
|
|
84
|
+
installPlugin().catch((err) => {
|
|
85
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
86
|
+
process.exitCode = 1;
|
|
87
|
+
});
|
package/package.json
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "robloxstudio-mcp",
|
|
3
|
-
"version": "2.5.0
|
|
3
|
+
"version": "2.5.0",
|
|
4
4
|
"description": "MCP Server for Roblox Studio Integration - Access Studio data, scripts, and objects through AI tools",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"bin": {
|
|
8
|
-
"robloxstudio-mcp": "dist/index.js"
|
|
8
|
+
"robloxstudio-mcp": "dist/index.js",
|
|
9
|
+
"robloxstudio-mcp-install-plugin": "dist/install-plugin-cli.js"
|
|
9
10
|
},
|
|
10
11
|
"files": [
|
|
11
12
|
"dist/**/*",
|
|
@@ -1333,6 +1333,7 @@ return {
|
|
|
1333
1333
|
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
1334
1334
|
local CaptureService = game:GetService("CaptureService")
|
|
1335
1335
|
local AssetService = game:GetService("AssetService")
|
|
1336
|
+
local MAX_TILE_SIZE = 1024
|
|
1336
1337
|
local BASE64_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
|
|
1337
1338
|
local function encodeBase64(buf)
|
|
1338
1339
|
local len = buffer.len(buf)
|
|
@@ -1362,6 +1363,59 @@ local function encodeBase64(buf)
|
|
|
1362
1363
|
end
|
|
1363
1364
|
return table.concat(parts, "")
|
|
1364
1365
|
end
|
|
1366
|
+
local function readPixelsTiled(img, w, h)
|
|
1367
|
+
local BYTES_PER_PIXEL = 4
|
|
1368
|
+
local fullBuf = buffer.create(w * h * BYTES_PER_PIXEL)
|
|
1369
|
+
local fullRowBytes = w * BYTES_PER_PIXEL
|
|
1370
|
+
do
|
|
1371
|
+
local ty = 0
|
|
1372
|
+
local _shouldIncrement = false
|
|
1373
|
+
while true do
|
|
1374
|
+
if _shouldIncrement then
|
|
1375
|
+
ty += MAX_TILE_SIZE
|
|
1376
|
+
else
|
|
1377
|
+
_shouldIncrement = true
|
|
1378
|
+
end
|
|
1379
|
+
if not (ty < h) then
|
|
1380
|
+
break
|
|
1381
|
+
end
|
|
1382
|
+
local tileH = math.min(MAX_TILE_SIZE, h - ty)
|
|
1383
|
+
do
|
|
1384
|
+
local tx = 0
|
|
1385
|
+
local _shouldIncrement_1 = false
|
|
1386
|
+
while true do
|
|
1387
|
+
if _shouldIncrement_1 then
|
|
1388
|
+
tx += MAX_TILE_SIZE
|
|
1389
|
+
else
|
|
1390
|
+
_shouldIncrement_1 = true
|
|
1391
|
+
end
|
|
1392
|
+
if not (tx < w) then
|
|
1393
|
+
break
|
|
1394
|
+
end
|
|
1395
|
+
local tileW = math.min(MAX_TILE_SIZE, w - tx)
|
|
1396
|
+
local tileBuf = img:ReadPixelsBuffer(Vector2.new(tx, ty), Vector2.new(tileW, tileH))
|
|
1397
|
+
local tileRowBytes = tileW * BYTES_PER_PIXEL
|
|
1398
|
+
do
|
|
1399
|
+
local row = 0
|
|
1400
|
+
local _shouldIncrement_2 = false
|
|
1401
|
+
while true do
|
|
1402
|
+
if _shouldIncrement_2 then
|
|
1403
|
+
row += 1
|
|
1404
|
+
else
|
|
1405
|
+
_shouldIncrement_2 = true
|
|
1406
|
+
end
|
|
1407
|
+
if not (row < tileH) then
|
|
1408
|
+
break
|
|
1409
|
+
end
|
|
1410
|
+
buffer.copy(fullBuf, (ty + row) * fullRowBytes + tx * BYTES_PER_PIXEL, tileBuf, row * tileRowBytes, tileRowBytes)
|
|
1411
|
+
end
|
|
1412
|
+
end
|
|
1413
|
+
end
|
|
1414
|
+
end
|
|
1415
|
+
end
|
|
1416
|
+
end
|
|
1417
|
+
return fullBuf
|
|
1418
|
+
end
|
|
1365
1419
|
local function captureScreenshot()
|
|
1366
1420
|
local contentId
|
|
1367
1421
|
CaptureService:CaptureScreenshot(function(id)
|
|
@@ -1385,11 +1439,11 @@ local function captureScreenshot()
|
|
|
1385
1439
|
}
|
|
1386
1440
|
end
|
|
1387
1441
|
local editableImage = editableResult
|
|
1388
|
-
local
|
|
1389
|
-
local w = math.floor(
|
|
1390
|
-
local h = math.floor(
|
|
1442
|
+
local imgSize = editableImage.Size
|
|
1443
|
+
local w = math.floor(imgSize.X)
|
|
1444
|
+
local h = math.floor(imgSize.Y)
|
|
1391
1445
|
local readOk, pixelBuffer = pcall(function()
|
|
1392
|
-
return editableImage
|
|
1446
|
+
return readPixelsTiled(editableImage, w, h)
|
|
1393
1447
|
end)
|
|
1394
1448
|
editableImage:Destroy()
|
|
1395
1449
|
if not readOk then
|
|
@@ -2937,6 +2991,9 @@ local function getFileTree(requestData)
|
|
|
2937
2991
|
if instance:IsA("LuaSourceContainer") then
|
|
2938
2992
|
node.hasSource = true
|
|
2939
2993
|
node.scriptType = instance.ClassName
|
|
2994
|
+
if instance:IsA("BaseScript") then
|
|
2995
|
+
node.enabled = instance.Enabled
|
|
2996
|
+
end
|
|
2940
2997
|
end
|
|
2941
2998
|
for _, child in instance:GetChildren() do
|
|
2942
2999
|
local _children = node.children
|
|
@@ -2979,13 +3036,16 @@ local function searchFiles(requestData)
|
|
|
2979
3036
|
match = (string.find(_exp, _arg0)) ~= nil
|
|
2980
3037
|
end
|
|
2981
3038
|
if match then
|
|
2982
|
-
local
|
|
3039
|
+
local entry = {
|
|
2983
3040
|
name = instance.Name,
|
|
2984
3041
|
className = instance.ClassName,
|
|
2985
3042
|
path = getInstancePath(instance),
|
|
2986
3043
|
hasSource = instance:IsA("LuaSourceContainer"),
|
|
2987
3044
|
}
|
|
2988
|
-
|
|
3045
|
+
if instance:IsA("BaseScript") then
|
|
3046
|
+
entry.enabled = instance.Enabled
|
|
3047
|
+
end
|
|
3048
|
+
table.insert(results, entry)
|
|
2989
3049
|
end
|
|
2990
3050
|
for _, child in instance:GetChildren() do
|
|
2991
3051
|
searchRecursive(child)
|
|
@@ -3246,14 +3306,17 @@ local function getInstanceChildren(requestData)
|
|
|
3246
3306
|
end
|
|
3247
3307
|
local children = {}
|
|
3248
3308
|
for _, child in instance:GetChildren() do
|
|
3249
|
-
local
|
|
3309
|
+
local entry = {
|
|
3250
3310
|
name = child.Name,
|
|
3251
3311
|
className = child.ClassName,
|
|
3252
3312
|
path = getInstancePath(child),
|
|
3253
3313
|
hasChildren = #child:GetChildren() > 0,
|
|
3254
3314
|
hasSource = child:IsA("LuaSourceContainer"),
|
|
3255
3315
|
}
|
|
3256
|
-
|
|
3316
|
+
if child:IsA("BaseScript") then
|
|
3317
|
+
entry.enabled = child.Enabled
|
|
3318
|
+
end
|
|
3319
|
+
table.insert(children, entry)
|
|
3257
3320
|
end
|
|
3258
3321
|
return {
|
|
3259
3322
|
instancePath = instancePath,
|
|
@@ -3694,13 +3757,16 @@ local function grepScripts(requestData)
|
|
|
3694
3757
|
end
|
|
3695
3758
|
end
|
|
3696
3759
|
if scriptMatchCount > 0 then
|
|
3697
|
-
local
|
|
3760
|
+
local scriptResult = {
|
|
3698
3761
|
instancePath = getInstancePath(instance),
|
|
3699
3762
|
name = instance.Name,
|
|
3700
3763
|
className = instance.ClassName,
|
|
3701
3764
|
matches = scriptMatches,
|
|
3702
3765
|
}
|
|
3703
|
-
|
|
3766
|
+
if instance:IsA("BaseScript") then
|
|
3767
|
+
scriptResult.enabled = instance.Enabled
|
|
3768
|
+
end
|
|
3769
|
+
table.insert(results, scriptResult)
|
|
3704
3770
|
end
|
|
3705
3771
|
end
|
|
3706
3772
|
for _, child in instance:GetChildren() do
|
|
@@ -3766,6 +3832,7 @@ local function normalizeEscapes(s)
|
|
|
3766
3832
|
result = (string.gsub(result, "\\n", "\n"))
|
|
3767
3833
|
result = (string.gsub(result, "\\t", "\t"))
|
|
3768
3834
|
result = (string.gsub(result, "\\r", "\r"))
|
|
3835
|
+
result = (string.gsub(result, '\\"', '"'))
|
|
3769
3836
|
result = (string.gsub(result, "\\\\", "\\"))
|
|
3770
3837
|
return result
|
|
3771
3838
|
end
|
|
@@ -4449,7 +4516,7 @@ return {
|
|
|
4449
4516
|
<Properties>
|
|
4450
4517
|
<string name="Name">State</string>
|
|
4451
4518
|
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
4452
|
-
local CURRENT_VERSION = "2.5.0
|
|
4519
|
+
local CURRENT_VERSION = "2.5.0"
|
|
4453
4520
|
local MAX_CONNECTIONS = 5
|
|
4454
4521
|
local BASE_PORT = 58741
|
|
4455
4522
|
local activeTabIndex = 0
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
const CaptureService = game.GetService("CaptureService");
|
|
2
2
|
const AssetService = game.GetService("AssetService");
|
|
3
3
|
|
|
4
|
+
const MAX_TILE_SIZE = 1024;
|
|
4
5
|
const BASE64_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
5
6
|
|
|
6
7
|
function encodeBase64(buf: buffer): string {
|
|
@@ -46,6 +47,25 @@ function encodeBase64(buf: buffer): string {
|
|
|
46
47
|
return parts.join("");
|
|
47
48
|
}
|
|
48
49
|
|
|
50
|
+
function readPixelsTiled(img: EditableImage, w: number, h: number): buffer {
|
|
51
|
+
const BYTES_PER_PIXEL = 4;
|
|
52
|
+
const fullBuf = buffer.create(w * h * BYTES_PER_PIXEL);
|
|
53
|
+
const fullRowBytes = w * BYTES_PER_PIXEL;
|
|
54
|
+
|
|
55
|
+
for (let ty = 0; ty < h; ty += MAX_TILE_SIZE) {
|
|
56
|
+
const tileH = math.min(MAX_TILE_SIZE, h - ty);
|
|
57
|
+
for (let tx = 0; tx < w; tx += MAX_TILE_SIZE) {
|
|
58
|
+
const tileW = math.min(MAX_TILE_SIZE, w - tx);
|
|
59
|
+
const tileBuf = img.ReadPixelsBuffer(new Vector2(tx, ty), new Vector2(tileW, tileH));
|
|
60
|
+
const tileRowBytes = tileW * BYTES_PER_PIXEL;
|
|
61
|
+
for (let row = 0; row < tileH; row++) {
|
|
62
|
+
buffer.copy(fullBuf, (ty + row) * fullRowBytes + tx * BYTES_PER_PIXEL, tileBuf, row * tileRowBytes, tileRowBytes);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return fullBuf;
|
|
67
|
+
}
|
|
68
|
+
|
|
49
69
|
function captureScreenshot(): unknown {
|
|
50
70
|
let contentId: string | undefined;
|
|
51
71
|
|
|
@@ -74,12 +94,12 @@ function captureScreenshot(): unknown {
|
|
|
74
94
|
}
|
|
75
95
|
|
|
76
96
|
const editableImage = editableResult as EditableImage;
|
|
77
|
-
const
|
|
78
|
-
const w = math.floor(
|
|
79
|
-
const h = math.floor(
|
|
97
|
+
const imgSize = editableImage.Size;
|
|
98
|
+
const w = math.floor(imgSize.X);
|
|
99
|
+
const h = math.floor(imgSize.Y);
|
|
80
100
|
|
|
81
101
|
const [readOk, pixelBuffer] = pcall(() => {
|
|
82
|
-
return editableImage
|
|
102
|
+
return readPixelsTiled(editableImage, w, h);
|
|
83
103
|
});
|
|
84
104
|
|
|
85
105
|
editableImage.Destroy();
|
|
@@ -9,6 +9,7 @@ interface TreeNode {
|
|
|
9
9
|
children: TreeNode[];
|
|
10
10
|
hasSource?: boolean;
|
|
11
11
|
scriptType?: string;
|
|
12
|
+
enabled?: boolean;
|
|
12
13
|
}
|
|
13
14
|
|
|
14
15
|
function getFileTree(requestData: Record<string, unknown>) {
|
|
@@ -34,6 +35,9 @@ function getFileTree(requestData: Record<string, unknown>) {
|
|
|
34
35
|
if (instance.IsA("LuaSourceContainer")) {
|
|
35
36
|
node.hasSource = true;
|
|
36
37
|
node.scriptType = instance.ClassName;
|
|
38
|
+
if (instance.IsA("BaseScript")) {
|
|
39
|
+
node.enabled = instance.Enabled;
|
|
40
|
+
}
|
|
37
41
|
}
|
|
38
42
|
|
|
39
43
|
for (const child of instance.GetChildren()) {
|
|
@@ -55,7 +59,7 @@ function searchFiles(requestData: Record<string, unknown>) {
|
|
|
55
59
|
|
|
56
60
|
if (!query) return { error: "Query is required" };
|
|
57
61
|
|
|
58
|
-
const results: { name: string; className: string; path: string; hasSource: boolean }[] = [];
|
|
62
|
+
const results: { name: string; className: string; path: string; hasSource: boolean; enabled?: boolean }[] = [];
|
|
59
63
|
|
|
60
64
|
function searchRecursive(instance: Instance) {
|
|
61
65
|
let match = false;
|
|
@@ -69,12 +73,16 @@ function searchFiles(requestData: Record<string, unknown>) {
|
|
|
69
73
|
}
|
|
70
74
|
|
|
71
75
|
if (match) {
|
|
72
|
-
|
|
76
|
+
const entry: { name: string; className: string; path: string; hasSource: boolean; enabled?: boolean } = {
|
|
73
77
|
name: instance.Name,
|
|
74
78
|
className: instance.ClassName,
|
|
75
79
|
path: getInstancePath(instance),
|
|
76
80
|
hasSource: instance.IsA("LuaSourceContainer"),
|
|
77
|
-
}
|
|
81
|
+
};
|
|
82
|
+
if (instance.IsA("BaseScript")) {
|
|
83
|
+
entry.enabled = instance.Enabled;
|
|
84
|
+
}
|
|
85
|
+
results.push(entry);
|
|
78
86
|
}
|
|
79
87
|
|
|
80
88
|
for (const child of instance.GetChildren()) {
|
|
@@ -300,15 +308,19 @@ function getInstanceChildren(requestData: Record<string, unknown>) {
|
|
|
300
308
|
const instance = getInstanceByPath(instancePath);
|
|
301
309
|
if (!instance) return { error: `Instance not found: ${instancePath}` };
|
|
302
310
|
|
|
303
|
-
const children: { name: string; className: string; path: string; hasChildren: boolean; hasSource: boolean }[] = [];
|
|
311
|
+
const children: { name: string; className: string; path: string; hasChildren: boolean; hasSource: boolean; enabled?: boolean }[] = [];
|
|
304
312
|
for (const child of instance.GetChildren()) {
|
|
305
|
-
|
|
313
|
+
const entry: { name: string; className: string; path: string; hasChildren: boolean; hasSource: boolean; enabled?: boolean } = {
|
|
306
314
|
name: child.Name,
|
|
307
315
|
className: child.ClassName,
|
|
308
316
|
path: getInstancePath(child),
|
|
309
317
|
hasChildren: child.GetChildren().size() > 0,
|
|
310
318
|
hasSource: child.IsA("LuaSourceContainer"),
|
|
311
|
-
}
|
|
319
|
+
};
|
|
320
|
+
if (child.IsA("BaseScript")) {
|
|
321
|
+
entry.enabled = child.Enabled;
|
|
322
|
+
}
|
|
323
|
+
children.push(entry);
|
|
312
324
|
}
|
|
313
325
|
|
|
314
326
|
return { instancePath, children, count: children.size() };
|
|
@@ -568,6 +580,7 @@ function grepScripts(requestData: Record<string, unknown>) {
|
|
|
568
580
|
instancePath: string;
|
|
569
581
|
name: string;
|
|
570
582
|
className: string;
|
|
583
|
+
enabled?: boolean;
|
|
571
584
|
matches: LineMatch[];
|
|
572
585
|
}
|
|
573
586
|
|
|
@@ -644,12 +657,16 @@ function grepScripts(requestData: Record<string, unknown>) {
|
|
|
644
657
|
}
|
|
645
658
|
|
|
646
659
|
if (scriptMatchCount > 0) {
|
|
647
|
-
|
|
660
|
+
const scriptResult: ScriptResult = {
|
|
648
661
|
instancePath: getInstancePath(instance),
|
|
649
662
|
name: instance.Name,
|
|
650
663
|
className: instance.ClassName,
|
|
651
664
|
matches: scriptMatches,
|
|
652
|
-
}
|
|
665
|
+
};
|
|
666
|
+
if (instance.IsA("BaseScript")) {
|
|
667
|
+
scriptResult.enabled = instance.Enabled;
|
|
668
|
+
}
|
|
669
|
+
results.push(scriptResult);
|
|
653
670
|
}
|
|
654
671
|
}
|
|
655
672
|
|
|
@@ -11,6 +11,7 @@ function normalizeEscapes(s: string): string {
|
|
|
11
11
|
result = result.gsub("\\n", "\n")[0];
|
|
12
12
|
result = result.gsub("\\t", "\t")[0];
|
|
13
13
|
result = result.gsub("\\r", "\r")[0];
|
|
14
|
+
result = result.gsub('\\"', '"')[0];
|
|
14
15
|
result = result.gsub("\\\\", "\\")[0];
|
|
15
16
|
return result;
|
|
16
17
|
}
|