├── fxmanifest.lua ├── client.lua ├── README.md └── html ├── index.html ├── css └── style.css └── js └── script.js /fxmanifest.lua: -------------------------------------------------------------------------------- 1 | fx_version 'cerulean' 2 | games {"rdr3", "gta5"} 3 | rdr3_warning 'I acknowledge that this is a prerelease build of RedM, and I am aware my resources *will* become incompatible once RedM ships.' 4 | 5 | lua54 'yes' 6 | 7 | author 'Blaze Scripts' 8 | description 'Interactive Lua table viewer for debugging with advanced search and JSON parsing' 9 | version '1.0.0' 10 | 11 | client_scripts { 12 | 'client.lua', 13 | } 14 | 15 | ui_page 'html/index.html' 16 | 17 | files { 18 | 'html/index.html', 19 | 'html/css/style.css', 20 | 'html/js/script.js', 21 | } -------------------------------------------------------------------------------- /client.lua: -------------------------------------------------------------------------------- 1 | local isTableViewerOpen = false 2 | local currentTableData = nil 3 | local resourceVersion = GetResourceMetadata(GetCurrentResourceName(), 'version', 0) or '1.0.0' 4 | 5 | ---@param table any The table to be viewed 6 | ---@param title string Optional title for the table view 7 | ---@return boolean Success status 8 | local function viewTable(table, title) 9 | if type(table) ~= "table" then 10 | print("bs-tableviewer: Input is not a table") 11 | return false 12 | end 13 | 14 | local success, result = pcall(function() 15 | return json.encode(table) 16 | end) 17 | 18 | if not success then 19 | print("bs-tableviewer: Failed to encode table to JSON") 20 | return false 21 | end 22 | 23 | currentTableData = result 24 | isTableViewerOpen = true 25 | 26 | SendNUIMessage({ 27 | action = 'openTableViewer', 28 | data = result, 29 | title = title, 30 | version = resourceVersion 31 | }) 32 | 33 | SetNuiFocus(true, true) 34 | return true 35 | end 36 | 37 | RegisterNUICallback('closeTableViewer', function(_, cb) 38 | isTableViewerOpen = false 39 | SetNuiFocus(false, false) 40 | currentTableData = nil 41 | cb('ok') 42 | end) 43 | 44 | exports('debugTable', viewTable) 45 | 46 | RegisterCommand('testviewer', function() 47 | local testTable = { 48 | name = "Test Table", 49 | nested = { 50 | value = 123, 51 | array = {1, 2, 3, 4, 5}, 52 | deep = { 53 | deeper = { 54 | deepest = "Hello World" 55 | } 56 | } 57 | }, 58 | array = {"a", "b", "c"}, 59 | boolean = true, 60 | number = 42 61 | } 62 | 63 | viewTable(testTable, "Test Table Viewer") 64 | end, false) 65 | 66 | RegisterNetEvent('debugTable') 67 | AddEventHandler('debugTable', function(tableData, title) 68 | viewTable(tableData, title) 69 | end) -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Interactive Lua Table Debugger for FiveM/RedM 2 | 3 | A powerful Lua table visualization tool for FiveM / RedM development. With just a short event you can debug your table - easy and plug-and-play. This Dev Tool provides a clean, interactive interface for viewing complex nested Lua tables, making debugging and development significantly easier. 4 | 5 | ## ✨ Features 6 | 7 | - 🔌 **Standalone**: Plug-and-play for FiveM and RedM development with no dependencies 8 | - 🧩 **Lightweight Design**: Lightweight and easy to use, fits your individual development workflow 9 | - 🍑 **Simple Integration**: Easy to use from any client or server script 10 | - 🔄 **Dual View Mode**: Toggle between structured tree view and raw JSON view 11 | - 🧠 **Smart JSON Parsing**: Automatically detects and parses nested JSON strings 12 | - 🔎 **Advanced Search**: Search in both tree and JSON views with scrollbar markers for easy navigation 13 | - 💯 **Interactive Table Exploration**: Expand/collapse nested tables with ease 14 | - 📋 **Copy Options**: Copy full JSON, specific paths, or individual values via context menu 15 | - ⌨️ **Keyboard Shortcuts**: Quickly navigate and control the viewer 16 | 17 | ## 📝 Preview 18 | 19 | ![image](https://i.imgur.com/PsvmkZi.jpeg) 20 | ![image](https://i.imgur.com/s3d2dja.jpeg) 21 | ![image](https://i.imgur.com/szOZrk1.jpeg) 22 | 23 | 24 | ## 🔧 Installation 25 | 26 | 1. Place the `bs-tableviewer` folder in your server's resources directory 27 | 2. Add `ensure bs-tableviewer` to your server.cfg 28 | 3. Restart your server or start the resource manually 29 | 30 | ## 📦 Exports / API 31 | 32 | ### From Client Scripts 33 | 34 | ```lua 35 | -- Using event 36 | TriggerEvent('debugTable', myTable) 37 | 38 | -- Using export (recommended) 39 | exports['bs-tableviewer']:debugTable(myTable) 40 | 41 | -- With custom title 42 | exports['bs-tableviewer']:debugTable(myTable, "My Custom Title") -- optional title 43 | ``` 44 | 45 | ### From Server Scripts 46 | 47 | ```lua 48 | -- Send table to specific player 49 | TriggerClientEvent('debugTable', source, myTable) 50 | 51 | -- Example with player data serverside 52 | local Player = exports.qbx_core:GetPlayer(source) 53 | TriggerClientEvent('debugTable', source, Player.PlayerData, "Player Data") -- optional title 54 | ``` 55 | 56 | ### Test Commands 57 | 58 | - `/testviewer` - Shows a test table for demonstration purposes 59 | 60 | ## ⌨️ Keyboard Shortcuts 61 | 62 | | Shortcut | Action | 63 | |----------|--------| 64 | | `Esc` | Close the viewer | 65 | | `Ctrl+F` | Focus search box | 66 | | `Ctrl+J` | Toggle between tree and JSON views | 67 | 68 | ## 🔥 Advanced Features 69 | 70 | ### JSON String Parsing 71 | 72 | The table viewer automatically detects and parses JSON strings within your data, making it easier to navigate complex nested structures. Parsed JSON strings are marked with a "parsed" badge in the tree view. 73 | 74 | ### Search with Scrollbar Markers 75 | 76 | When searching in JSON view, markers appear on the scrollbar showing the positions of all matches, allowing you to quickly navigate between search results by clicking on the markers. 77 | 78 | ### Context Menu 79 | 80 | Right-click on any item to access additional options: 81 | - **Copy Path**: Copies the Lua-compatible path to the item 82 | - **Copy Value**: Copies the value of the item 83 | 84 | ## 🧰 Dependencies 85 | 86 | This resource is completely standalone with no external dependencies, making it extremely lightweight and easy to integrate into any server. 87 | 88 | ## 🤝 Support 89 | For support, join our Discord server: https://discord.gg/xUcj2R4ZX4 90 | > At Blaze Scripts, we’re here to make your life easier - so if you ever run into a hiccup with one of our resources, just reach out and we’ll jump in to help. We strive to support all major frameworks, but on the rare occasion you’re using something outside our usual expertise, we might not be able to provide direct assistance. Either way, we’ll always point you toward resources or guidance to keep your project moving forward. 91 | 92 | ## 🔗 Links 93 | 94 | - 🧾 GitHub: [Blaze Scripts](https://github.com/Blaze-Scripts/) 95 | - 💬 Discord: [Join our Discord](https://discord.gg/xUcj2R4ZX4) 96 | - 👀 More Free Scripts: [Blaze Scripts](https://github.com/Blaze-Scripts/) 97 | 98 | ## License 99 | 100 | This resource is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. -------------------------------------------------------------------------------- /html/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Blaze Scripts Table Viewer 7 | 8 | 9 | 10 | 11 | 12 |
13 |
14 |
15 |

{{ title }}

16 |
17 |
18 | 19 | 20 |
21 |
22 | 23 | 24 | 25 | 26 | 27 |
28 |
29 |
30 |
31 |
32 | 37 |
38 |
39 |
40 |

 41 |                         

 42 |                     
43 |
44 |
49 |
50 |
51 |
52 |
53 | 64 |
65 | 66 |
67 |
68 | Copy Path 69 |
70 |
71 | Copy Value 72 |
73 |
74 |
75 | 76 | 99 | 100 | 101 | 102 | 103 | -------------------------------------------------------------------------------- /html/css/style.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --primary-color: rgb(41, 43, 48); 3 | --secondary-color: #858aa0; 4 | --background-color: rgba(0, 0, 0, 0.85); 5 | --text-color: #e0e0e0; 6 | --border-color: #4e4e4e; 7 | --item-hover: rgba(255, 255, 255, 0.1); 8 | --item-active: rgba(255, 255, 255, 0.082); 9 | 10 | --type-string: rgba(16, 185, 129, 0.2); 11 | --type-string-text: rgb(34, 255, 181); 12 | 13 | --type-number: rgba(245, 158, 11, 0.2); 14 | --type-number-text: rgb(255, 196, 86); 15 | 16 | --type-boolean: rgba(139, 93, 246, 0.2); 17 | --type-boolean-text: rgb(178, 145, 255); 18 | 19 | --type-table: rgba(87, 112, 255, 0.2); 20 | --type-table-text: rgb(144, 160, 255); 21 | 22 | --type-function: rgba(236, 72, 153, 0.2); 23 | --type-function-text: rgb(255, 103, 179); 24 | 25 | --type-nil: rgba(107, 114, 128, 0.2); 26 | --type-nil-text: rgb(231, 231, 231); 27 | 28 | --type-version: rgba(52, 255, 34, 0.2); 29 | --type-version-text: rgb(52, 255, 34); 30 | } 31 | 32 | ::selection { 33 | color: var(--type-version-text); 34 | background: var(--type-version); 35 | } 36 | 37 | * { 38 | margin: 0; 39 | padding: 0; 40 | box-sizing: border-box; 41 | font-family: monospace; 42 | } 43 | 44 | body { 45 | width: 100vw; 46 | height: 100vh; 47 | overflow: hidden; 48 | background-color: transparent; 49 | } 50 | 51 | #app { 52 | width: 100%; 53 | height: 100%; 54 | display: flex; 55 | justify-content: center; 56 | align-items: center; 57 | } 58 | 59 | #table-viewer { 60 | width: 80%; 61 | max-width: 1200px; 62 | height: 80%; 63 | background-color: var(--background-color); 64 | border-radius: 10px; 65 | box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5); 66 | display: flex; 67 | flex-direction: column; 68 | overflow: hidden; 69 | } 70 | 71 | /* Header */ 72 | .header { 73 | display: flex; 74 | justify-content: space-between; 75 | align-items: center; 76 | padding: 15px 20px; 77 | color: var(--text-color); 78 | border-bottom: 1px solid var(--border-color); 79 | } 80 | 81 | .header h1 { 82 | font-size: 20px; 83 | margin-right: 20px; 84 | font-weight: 600; 85 | } 86 | 87 | .controls { 88 | display: flex; 89 | align-items: center; 90 | flex: 1; 91 | justify-content: space-between; 92 | } 93 | 94 | .search-container { 95 | position: relative; 96 | display: flex; 97 | align-items: center; 98 | max-width: 300px; 99 | width: 100%; 100 | } 101 | 102 | .search-container i { 103 | position: absolute; 104 | left: 10px; 105 | color: var(--secondary-color); 106 | } 107 | 108 | .search-container input { 109 | width: 100%; 110 | padding: 8px 10px 8px 35px; 111 | border: none; 112 | border-radius: 4px; 113 | background-color: rgba(255, 255, 255, 0.1); 114 | color: var(--text-color); 115 | font-family: monospace; 116 | } 117 | 118 | .search-container input:focus { 119 | outline: none; 120 | background-color: rgba(255, 255, 255, 0.15); 121 | } 122 | 123 | .buttons { 124 | display: flex; 125 | gap: 5px; 126 | } 127 | 128 | .controls button { 129 | cursor: pointer; 130 | padding: 8px 12px; 131 | border-radius: 4px; 132 | transition: all 0.2s; 133 | display: flex; 134 | align-items: center; 135 | justify-content: center; 136 | } 137 | 138 | .controls button:hover { 139 | background-color: var(--item-hover); 140 | transform: scale(1.05); 141 | } 142 | 143 | .controls button:active { 144 | transform: scale(0.95); 145 | } 146 | 147 | /* Content */ 148 | .content { 149 | flex: 1; 150 | overflow-y: auto; 151 | padding: 10px; 152 | } 153 | 154 | .json-view-container { 155 | position: relative; 156 | height: 100%; 157 | width: 100%; 158 | } 159 | 160 | .json-view { 161 | height: 100%; 162 | overflow: auto; 163 | width: 100%; 164 | } 165 | 166 | .scrollbar-markers { 167 | position: absolute; 168 | right: 0; 169 | top: 0; 170 | width: 12px; 171 | height: 100%; 172 | pointer-events: none; 173 | z-index: 10; 174 | } 175 | 176 | .search-marker { 177 | position: absolute; 178 | right: 2px; 179 | width: 6px; 180 | height: 3px; 181 | background-color: rgba(234, 179, 8, 0.8); 182 | border-radius: 1px; 183 | pointer-events: auto; 184 | cursor: pointer; 185 | transition: width 0.1s ease, right 0.1s ease; 186 | } 187 | 188 | .search-marker:hover { 189 | width: 10px; 190 | right: 0; 191 | background-color: rgba(234, 179, 8, 0.75); 192 | } 193 | 194 | .json-view pre { 195 | font-family: monospace; 196 | white-space: pre-wrap; 197 | word-wrap: break-word; 198 | color: var(--text-color); 199 | line-height: 1.5; 200 | padding: 5px; 201 | } 202 | 203 | /* Tree View */ 204 | .tree-view { 205 | padding: 0 20px; 206 | } 207 | 208 | .tree-item { 209 | margin: 2px 0; 210 | border-radius: 4px; 211 | } 212 | 213 | .tree-item-header { 214 | display: flex; 215 | align-items: flex-start; 216 | padding: 6px 10px; 217 | cursor: pointer; 218 | border-radius: 4px; 219 | transition: background-color 0.2s; 220 | color: var(--text-color); 221 | } 222 | 223 | .tree-item-header:hover { 224 | background-color: var(--item-hover); 225 | } 226 | 227 | .tree-item-header.expanded { 228 | background-color: var(--item-active); 229 | } 230 | 231 | .toggle-icon { 232 | margin-right: 8px; 233 | width: 16px; 234 | text-align: center; 235 | } 236 | 237 | .key { 238 | font-weight: 600; 239 | margin-right: 8px; 240 | } 241 | 242 | .type-badge { 243 | font-size: 12px; 244 | padding: 2px 6px; 245 | border-radius: 4px; 246 | margin-right: 8px; 247 | } 248 | 249 | .type-string { 250 | background-color: var(--type-string); 251 | color: var(--type-string-text); 252 | } 253 | 254 | .type-number { 255 | background-color: var(--type-number); 256 | color: var(--type-number-text); 257 | } 258 | 259 | .type-boolean { 260 | background-color: var(--type-boolean); 261 | color: var(--type-boolean-text); 262 | } 263 | 264 | .type-table { 265 | background-color: var(--type-table); 266 | color: var(--type-table-text); 267 | } 268 | 269 | .type-function { 270 | background-color: var(--type-function); 271 | color: var(--type-function-text); 272 | } 273 | 274 | .type-nil { 275 | background-color: var(--type-nil); 276 | color: var(--type-nil-text); 277 | } 278 | 279 | .type-version{ 280 | background-color: var(--type-version); 281 | color: var(--type-version-text); 282 | } 283 | 284 | .badge-blue { 285 | background-color: rgba(34, 222, 255, 0.2); 286 | color: rgb(34, 222, 255); 287 | } 288 | 289 | .badge-yellow { 290 | background-color: rgba(255, 196, 86, 0.2); 291 | color: rgb(255, 196, 86); 292 | } 293 | 294 | .badge-violet { 295 | background-color: rgba(186, 157, 255, 0.2); 296 | color: rgb(186, 157, 255); 297 | } 298 | 299 | .badge-grey { 300 | background-color: rgba(231, 231, 231, 0.2); 301 | color: rgb(231, 231, 231); 302 | } 303 | 304 | .badge-red { 305 | background-color: rgba(255, 103, 103, 0.2); 306 | color: rgb(255, 103, 103); 307 | } 308 | 309 | .json-parsed-badge { 310 | background-color: rgba(229, 70, 203, 0.2); 311 | color: rgb(255, 103, 179); 312 | font-size: 11px; 313 | padding: 1px 5px; 314 | border-radius: 4px; 315 | margin-right: 8px; 316 | font-style: italic; 317 | } 318 | 319 | .value { 320 | font-family: monospace; 321 | word-break: break-all; 322 | } 323 | 324 | span.value { 325 | background: none; 326 | } 327 | 328 | .summary { 329 | opacity: 0.7; 330 | font-style: italic; 331 | } 332 | 333 | .tree-item-children { 334 | padding-left: 20px; 335 | border-left: 1px dashed var(--border-color); 336 | margin-left: 10px; 337 | } 338 | 339 | /* Search highlighting */ 340 | .search-match > .tree-item-header { 341 | background-color: rgba(234, 179, 8, 0.2); 342 | border: 1px solid rgba(234, 179, 8, 0.4); 343 | } 344 | 345 | .search-highlight { 346 | background-color: rgba(234, 179, 8, 0.2); 347 | border: 1px solid rgba(234, 179, 8, 0.4); 348 | color: rgba(234, 179, 8, 1); 349 | font-weight: bold; 350 | border-radius: 2px; 351 | } 352 | 353 | /* Footer */ 354 | .footer { 355 | display: flex; 356 | justify-content: space-between; 357 | padding: 10px; 358 | background-color: var(--header-bg-color); 359 | color: var(--secondary-color); 360 | font-size: 12px; 361 | border-top: 1px solid var(--border-color); 362 | } 363 | 364 | .footer-left, .footer-right { 365 | display: flex; 366 | align-items: center; 367 | } 368 | 369 | .footer-left span { 370 | margin-right: 15px; 371 | } 372 | 373 | .footer-right span { 374 | margin-left: 15px; 375 | display: flex; 376 | align-items: center; 377 | } 378 | 379 | kbd { 380 | background-color: rgba(255, 255, 255, 0.1); 381 | border: 1px solid rgba(255, 255, 255, 0.2); 382 | border-radius: 3px; 383 | box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2); 384 | color: #fff; 385 | display: inline-block; 386 | font-family: monospace; 387 | font-size: 11px; 388 | line-height: 1; 389 | padding: 2px 4px; 390 | margin: 0 3px; 391 | } 392 | 393 | /* Context Menu */ 394 | #context-menu { 395 | position: fixed; 396 | z-index: 1000; 397 | background-color: var(--background-color); 398 | border: 1px solid var(--border-color); 399 | border-radius: 4px; 400 | box-shadow: 0 2px 10px rgba(0, 0, 0, 0.3); 401 | min-width: 150px; 402 | overflow: hidden; 403 | } 404 | 405 | .context-menu-item { 406 | padding: 8px 12px; 407 | cursor: pointer; 408 | color: var(--text-color); 409 | font-size: 14px; 410 | display: flex; 411 | align-items: center; 412 | gap: 8px; 413 | transition: background-color 0.2s; 414 | } 415 | 416 | .context-menu-item:hover { 417 | background-color: var(--item-hover); 418 | } 419 | 420 | .context-menu-item i { 421 | width: 16px; 422 | text-align: center; 423 | color: var(--secondary-color); 424 | } 425 | 426 | /* Scrollbar */ 427 | ::-webkit-scrollbar { 428 | width: 8px; 429 | } 430 | 431 | ::-webkit-scrollbar-track { 432 | background: rgba(0, 0, 0, 0.1); 433 | } 434 | 435 | ::-webkit-scrollbar-thumb { 436 | background: rgba(255, 255, 255, 0.2); 437 | border-radius: 4px; 438 | } 439 | 440 | ::-webkit-scrollbar-thumb:hover { 441 | background: rgba(255, 255, 255, 0.3); 442 | } -------------------------------------------------------------------------------- /html/js/script.js: -------------------------------------------------------------------------------- 1 | const TreeItem = { 2 | name: 'TreeItem', 3 | template: '#tree-item-template', 4 | props: { 5 | item: Object, 6 | searchQuery: String, 7 | expanded: Object 8 | }, 9 | computed: { 10 | isObject() { 11 | return this.item && this.item.type === 'table' && this.item.children && this.item.children.length > 0; 12 | }, 13 | isExpanded() { 14 | if (this.searchQuery && this.hasMatchInChildren()) { 15 | this.expanded[this.item.path] = true; 16 | } 17 | return this.item && this.expanded[this.item.path] || false; 18 | }, 19 | typeBadgeClass() { 20 | return this.item ? `type-${this.item.type}` : ''; 21 | }, 22 | isMatch() { 23 | if (!this.searchQuery || !this.item) return false; 24 | const query = this.searchQuery.toLowerCase(); 25 | const key = this.item.key ? this.item.key.toLowerCase() : ''; 26 | const value = this.item.value ? String(this.item.value).toLowerCase() : ''; 27 | return key.includes(query) || value.includes(query); 28 | } 29 | }, 30 | methods: { 31 | toggle() { 32 | if (!this.isObject || !this.item) return; 33 | this.expanded[this.item.path] = !this.isExpanded; 34 | this.$forceUpdate(); 35 | }, 36 | getSummary() { 37 | if (!this.item || !this.item.children) return ''; 38 | 39 | const count = this.item.children.length; 40 | return `{ ${count} ${count === 1 ? 'item' : 'items'} }`; 41 | }, 42 | hasMatchInChildren() { 43 | if (!this.searchQuery || !this.item || !this.item.children) return false; 44 | 45 | const query = this.searchQuery.toLowerCase(); 46 | 47 | if (this.isMatch) return true; 48 | 49 | const checkChildren = (children) => { 50 | if (!children) return false; 51 | 52 | for (const child of children) { 53 | const key = child.key ? child.key.toLowerCase() : ''; 54 | const value = child.value ? String(child.value).toLowerCase() : ''; 55 | 56 | if (key.includes(query) || value.includes(query)) { 57 | return true; 58 | } 59 | 60 | if (child.type === 'table' && child.children && child.children.length > 0) { 61 | if (checkChildren(child.children)) { 62 | return true; 63 | } 64 | } 65 | } 66 | 67 | return false; 68 | }; 69 | 70 | return checkChildren(this.item.children); 71 | }, 72 | 73 | showContextMenu(event) { 74 | let path = this.item.path; 75 | 76 | if (path.startsWith('root.')) { 77 | path = path.substring(5); 78 | } 79 | 80 | const value = this.item.value; 81 | this.$root.showContextMenu(event.clientX, event.clientY, path, value); 82 | } 83 | } 84 | }; 85 | 86 | // Main App 87 | const app = Vue.createApp({ 88 | data() { 89 | return { 90 | visible: false, 91 | title: 'Blaze Scripts: Table Viewer', 92 | version: 'v1.0.0', 93 | rawData: null, 94 | parsedData: { 95 | key: 'root', 96 | type: 'table', 97 | path: 'root', 98 | children: [] 99 | }, 100 | searchQuery: '', 101 | expandedState: {}, 102 | showJsonView: false, 103 | formattedJson: '', 104 | highlightedJson: '', 105 | searchMarkers: [], 106 | contextMenuVisible: false, 107 | contextMenuStyle: { 108 | top: '0px', 109 | left: '0px' 110 | }, 111 | contextMenuPath: '', 112 | contextMenuValue: undefined 113 | }; 114 | }, 115 | computed: { 116 | highlightedJsonWithSearch() { 117 | if (!this.searchQuery || !this.formattedJson) return this.formattedJson; 118 | 119 | try { 120 | const searchRegex = new RegExp(this.escapeRegExp(this.searchQuery), 'gi'); 121 | let match; 122 | let matches = []; 123 | let lastIndex = 0; 124 | 125 | while ((match = searchRegex.exec(this.formattedJson)) !== null) { 126 | matches.push({ 127 | index: match.index, 128 | text: match[0], 129 | length: match[0].length 130 | }); 131 | } 132 | 133 | this.$nextTick(() => { 134 | this.calculateSearchMarkers(matches); 135 | }); 136 | 137 | return this.formattedJson.replace(new RegExp(this.escapeRegExp(this.searchQuery), 'gi'), match => { 138 | return `${match}`; 139 | }); 140 | } catch (error) { 141 | console.error('Error highlighting JSON search:', error); 142 | return this.formattedJson; 143 | } 144 | } 145 | }, 146 | methods: { 147 | parseData(data, parentPath = '', originalData = null) { 148 | const result = { 149 | key: parentPath === '' ? 'root' : parentPath.split('.').pop(), 150 | type: 'table', 151 | path: parentPath === '' ? 'root' : parentPath, 152 | children: [], 153 | originalData: originalData 154 | }; 155 | 156 | try { 157 | if (Array.isArray(data)) { 158 | for (let i = 0; i < data.length; i++) { 159 | const luaIndex = i + 1; 160 | const childPath = parentPath === '' ? `root[${luaIndex}]` : `${parentPath}[${luaIndex}]`; 161 | const value = data[i]; 162 | 163 | if (typeof value === 'string' && value.length > 2) { 164 | try { 165 | if ((value.trim().startsWith('{') && value.trim().endsWith('}')) || 166 | (value.trim().startsWith('[') && value.trim().endsWith(']'))) { 167 | const parsedJson = JSON.parse(value); 168 | 169 | if (typeof parsedJson === 'object' && parsedJson !== null) { 170 | const nestedResult = this.parseData(parsedJson, childPath, value); 171 | nestedResult.wasJsonString = true; 172 | nestedResult.originalString = value; 173 | result.children.push(nestedResult); 174 | continue; 175 | } 176 | } 177 | } catch (e) { 178 | console.log('Failed to parse potential JSON string:', e); 179 | } 180 | } 181 | 182 | if (typeof value === 'object' && value !== null) { 183 | result.children.push(this.parseData(value, childPath)); 184 | } else { 185 | result.children.push({ 186 | key: i, 187 | type: value === null ? 'nil' : typeof value, 188 | value: value === null ? 'nil' : value, 189 | path: childPath 190 | }); 191 | } 192 | } 193 | } else { 194 | for (const key in data) { 195 | if (!data.hasOwnProperty(key)) continue; 196 | 197 | const childPath = parentPath === '' ? `root.${key}` : `${parentPath}.${key}`; 198 | const value = data[key]; 199 | 200 | if (typeof value === 'string' && value.length > 2) { 201 | try { 202 | if ((value.trim().startsWith('{') && value.trim().endsWith('}')) || 203 | (value.trim().startsWith('[') && value.trim().endsWith(']'))) { 204 | const parsedJson = JSON.parse(value); 205 | 206 | if (typeof parsedJson === 'object' && parsedJson !== null) { 207 | const nestedResult = this.parseData(parsedJson, childPath, value); 208 | nestedResult.key = key; 209 | nestedResult.wasJsonString = true; 210 | nestedResult.originalString = value; 211 | result.children.push(nestedResult); 212 | continue; 213 | } 214 | } 215 | } catch (e) { 216 | console.log('Failed to parse potential JSON string:', e); 217 | } 218 | } 219 | 220 | if (typeof value === 'object' && value !== null) { 221 | result.children.push(this.parseData(value, childPath)); 222 | } else { 223 | result.children.push({ 224 | key: key, 225 | type: value === null ? 'nil' : typeof value, 226 | value: value === null ? 'nil' : value, 227 | path: childPath 228 | }); 229 | } 230 | } 231 | } 232 | } catch (error) { 233 | console.error('Error parsing data:', error); 234 | } 235 | 236 | return result; 237 | }, 238 | 239 | openTableViewer(data, title) { 240 | try { 241 | this.showJsonView = false; 242 | this.formattedJson = ''; 243 | 244 | if (typeof data === 'string') { 245 | try { 246 | this.rawData = JSON.parse(data); 247 | } catch (jsonError) { 248 | console.error('Failed to parse JSON:', jsonError); 249 | this.rawData = { error: 'Invalid JSON data' }; 250 | } 251 | } else { 252 | this.rawData = data || {}; 253 | } 254 | 255 | try { 256 | this.formattedJson = JSON.stringify(this.rawData, null, 2); 257 | } catch (jsonError) { 258 | console.error('Error formatting JSON:', jsonError); 259 | this.formattedJson = 'Error formatting JSON'; 260 | } 261 | 262 | const parsedResult = this.parseData(this.rawData); 263 | if (parsedResult) { 264 | this.parsedData = parsedResult; 265 | } 266 | 267 | this.title = title || 'Blaze Scripts: Table Viewer'; 268 | this.visible = true; 269 | this.expandedState = { 'root': true }; 270 | } catch (error) { 271 | console.error('Error opening table viewer:', error); 272 | this.parsedData = { 273 | key: 'root', 274 | type: 'table', 275 | path: 'root', 276 | children: [{ 277 | key: 'error', 278 | type: 'string', 279 | value: 'Failed to parse data: ' + error.message, 280 | path: 'root.error' 281 | }] 282 | }; 283 | } 284 | }, 285 | 286 | reconstructWithOriginalStrings(node) { 287 | if (!node) return null; 288 | 289 | if (node.wasJsonString && node.originalString) { 290 | return node.originalString; 291 | } 292 | 293 | if (node.type === 'table' && node.children && node.children.length > 0) { 294 | const isArray = node.children.every(child => !isNaN(parseInt(child.key))); 295 | 296 | let result = isArray ? [] : {}; 297 | 298 | for (const child of node.children) { 299 | const value = this.reconstructWithOriginalStrings(child); 300 | if (isArray) { 301 | result.push(value); 302 | } else { 303 | result[child.key] = value; 304 | } 305 | } 306 | 307 | return result; 308 | } 309 | 310 | return node.value; 311 | }, 312 | 313 | processJsonStringsForView(data) { 314 | if (typeof data !== 'object' || data === null) return data; 315 | 316 | const result = Array.isArray(data) ? [] : {}; 317 | 318 | for (const key in data) { 319 | if (!data.hasOwnProperty(key)) continue; 320 | 321 | const value = data[key]; 322 | 323 | if (typeof value === 'string' && value.length > 2) { 324 | try { 325 | if ((value.trim().startsWith('{') && value.trim().endsWith('}')) || 326 | (value.trim().startsWith('[') && value.trim().endsWith(']'))) { 327 | const parsedJson = JSON.parse(value); 328 | result[key] = parsedJson; 329 | continue; 330 | } 331 | } catch (e) { 332 | console.log('Failed to parse potential JSON string:', e); 333 | } 334 | } 335 | 336 | if (typeof value === 'object' && value !== null) { 337 | result[key] = this.processJsonStringsForView(value); 338 | } else { 339 | result[key] = value; 340 | } 341 | } 342 | 343 | return result; 344 | }, 345 | 346 | toggleView() { 347 | this.showJsonView = !this.showJsonView; 348 | 349 | if (this.showJsonView) { 350 | try { 351 | const processedData = this.processJsonStringsForView(this.rawData); 352 | 353 | this.formattedJson = JSON.stringify(processedData, null, 2); 354 | } catch (error) { 355 | console.error('Error formatting JSON:', error); 356 | this.formattedJson = 'Error formatting JSON'; 357 | } 358 | } 359 | }, 360 | 361 | close() { 362 | this.visible = false; 363 | this.showJsonView = false; 364 | this.rawData = null; 365 | this.parsedData = null; 366 | this.searchQuery = ''; 367 | this.expandedState = {}; 368 | 369 | fetch('https://bs-tableviewer/closeTableViewer', { 370 | method: 'POST', 371 | headers: { 372 | 'Content-Type': 'application/json; charset=UTF-8', 373 | }, 374 | body: JSON.stringify({}) 375 | }); 376 | }, 377 | 378 | expandAll() { 379 | const expandAll = (node, state) => { 380 | if (node.type === 'table' && node.children) { 381 | state[node.path] = true; 382 | 383 | for (const child of node.children) { 384 | expandAll(child, state); 385 | } 386 | } 387 | }; 388 | 389 | const newState = {}; 390 | expandAll(this.parsedData, newState); 391 | this.expandedState = newState; 392 | }, 393 | 394 | collapseAll() { 395 | this.expandedState = { 'root': true }; 396 | }, 397 | 398 | showContextMenu(x, y, path, value) { 399 | this.contextMenuStyle = { 400 | top: `${y}px`, 401 | left: `${x}px` 402 | }; 403 | 404 | this.contextMenuPath = path; 405 | this.contextMenuValue = value; 406 | this.contextMenuVisible = true; 407 | 408 | setTimeout(() => { 409 | if (!this._boundHideContextMenu) { 410 | this._boundHideContextMenu = this.hideContextMenu.bind(this); 411 | } 412 | document.addEventListener('click', this._boundHideContextMenu); 413 | }, 100); 414 | }, 415 | 416 | hideContextMenu() { 417 | this.contextMenuVisible = false; 418 | if (this._boundHideContextMenu) { 419 | document.removeEventListener('click', this._boundHideContextMenu); 420 | } 421 | }, 422 | 423 | copyPath() { 424 | if (!this.contextMenuPath) return; 425 | 426 | try { 427 | const tempInput = document.createElement('input'); 428 | tempInput.value = this.contextMenuPath; 429 | document.body.appendChild(tempInput); 430 | 431 | tempInput.select(); 432 | tempInput.setSelectionRange(0, 99999); 433 | 434 | document.execCommand('copy'); 435 | 436 | document.body.removeChild(tempInput); 437 | 438 | this.showNotification(`Path copied: ${this.contextMenuPath}`); 439 | } catch (err) { 440 | console.error('Failed to copy path:', err); 441 | } 442 | 443 | this.hideContextMenu(); 444 | }, 445 | 446 | copyValue() { 447 | if (this.contextMenuValue === undefined) return; 448 | 449 | try { 450 | const valueStr = String(this.contextMenuValue); 451 | const tempInput = document.createElement('input'); 452 | tempInput.value = valueStr; 453 | document.body.appendChild(tempInput); 454 | tempInput.select(); 455 | tempInput.setSelectionRange(0, 99999); 456 | document.execCommand('copy'); 457 | document.body.removeChild(tempInput); 458 | this.showNotification(`Value copied: ${valueStr}`); 459 | } catch (err) { 460 | console.error('Failed to copy value:', err); 461 | } 462 | this.hideContextMenu(); 463 | }, 464 | 465 | escapeRegExp(string) { 466 | return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); 467 | }, 468 | 469 | calculateSearchMarkers(matches) { 470 | if (!this.$refs.jsonContent || matches.length === 0) { 471 | this.searchMarkers = []; 472 | return; 473 | } 474 | 475 | const contentElement = this.$refs.jsonContent; 476 | const totalHeight = contentElement.scrollHeight; 477 | 478 | this.searchMarkers = matches.map((match, index) => { 479 | const range = document.createRange(); 480 | const matchElements = contentElement.querySelectorAll('.search-highlight'); 481 | 482 | if (index < matchElements.length) { 483 | const matchElement = matchElements[index]; 484 | range.selectNodeContents(matchElement); 485 | const rect = range.getBoundingClientRect(); 486 | const position = (rect.top - contentElement.getBoundingClientRect().top + contentElement.scrollTop) / totalHeight * 100; 487 | 488 | return { 489 | index: index, 490 | position: position, 491 | element: matchElement 492 | }; 493 | } 494 | 495 | return null; 496 | }).filter(marker => marker !== null); 497 | }, 498 | 499 | scrollToMatch(index) { 500 | if (!this.$refs.jsonContent) return; 501 | 502 | const matchElements = this.$refs.jsonContent.querySelectorAll('.search-highlight'); 503 | if (index < matchElements.length) { 504 | const matchElement = matchElements[index]; 505 | matchElement.scrollIntoView({ behavior: 'smooth', block: 'center' }); 506 | 507 | const originalBackground = matchElement.style.backgroundColor; 508 | matchElement.style.backgroundColor = 'rgba(234, 179, 8, 0.75)'; 509 | setTimeout(() => { 510 | matchElement.style.backgroundColor = originalBackground; 511 | }, 1000); 512 | } 513 | }, 514 | 515 | showNotification(message) { 516 | const notification = document.createElement('div'); 517 | notification.textContent = message; 518 | notification.style.position = 'fixed'; 519 | notification.style.bottom = '20px'; 520 | notification.style.left = '50%'; 521 | notification.style.transform = 'translateX(-50%)'; 522 | notification.style.backgroundColor = 'rgba(16, 185, 129, 0.9)'; 523 | notification.style.color = 'white'; 524 | notification.style.padding = '10px 20px'; 525 | notification.style.borderRadius = '4px'; 526 | notification.style.zIndex = '1000'; 527 | 528 | document.body.appendChild(notification); 529 | 530 | setTimeout(() => { 531 | document.body.removeChild(notification); 532 | }, 2000); 533 | }, 534 | 535 | copyToClipboard() { 536 | try { 537 | const jsonString = JSON.stringify(this.rawData, null, 2); 538 | 539 | const tempTextarea = document.createElement('textarea'); 540 | tempTextarea.value = jsonString; 541 | tempTextarea.style.position = 'fixed'; 542 | tempTextarea.style.left = '-9999px'; 543 | document.body.appendChild(tempTextarea); 544 | 545 | tempTextarea.select(); 546 | tempTextarea.setSelectionRange(0, 99999999); 547 | 548 | document.execCommand('copy'); 549 | 550 | document.body.removeChild(tempTextarea); 551 | 552 | this.showNotification('JSON copied to clipboard!'); 553 | } catch (error) { 554 | console.error('Error copying to clipboard:', error); 555 | } 556 | }, 557 | 558 | handleMessage(event) { 559 | const data = event.data; 560 | 561 | if (data.action === 'openTableViewer') { 562 | if (data.version) { 563 | this.version = 'v' + data.version; 564 | } 565 | this.openTableViewer(data.data, data.title); 566 | } 567 | }, 568 | 569 | handleKeyDown(event) { 570 | 571 | if (!this.visible) return; 572 | 573 | if (event.key === 'Escape') { 574 | this.close(); 575 | event.preventDefault(); 576 | return; 577 | } 578 | 579 | if ((event.ctrlKey || event.metaKey) && event.key === 'f') { 580 | 581 | if (this.$refs.searchInput) { 582 | this.$refs.searchInput.focus(); 583 | event.preventDefault(); 584 | } 585 | return; 586 | } 587 | 588 | if ((event.ctrlKey || event.metaKey) && event.key === 'j') { 589 | this.toggleView(); 590 | event.preventDefault(); 591 | return; 592 | } 593 | } 594 | }, 595 | mounted() { 596 | window.addEventListener('message', this.handleMessage); 597 | document.addEventListener('keydown', this.handleKeyDown); 598 | }, 599 | 600 | beforeUnmount() { 601 | window.removeEventListener('message', this.handleMessage); 602 | document.removeEventListener('keydown', this.handleKeyDown); 603 | } 604 | }); 605 | 606 | app.component('tree-item', TreeItem); 607 | app.mount('#app'); --------------------------------------------------------------------------------