├── 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 | 
20 | 
21 | 
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 |
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');
--------------------------------------------------------------------------------