aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSan Jacobs2025-06-06 02:58:02 +0200
committerSan Jacobs2025-06-06 02:58:02 +0200
commite50cee75f11f0a9b1b4006daec2381bb497e02f6 (patch)
treeabca0062650ada9e0eb0e35e6f91f20d40d05057
parentf93340dd2c416ff60be64dfea261d8db9ef1aabb (diff)
downloadbetter-report-e50cee75f11f0a9b1b4006daec2381bb497e02f6.tar.gz
better-report-e50cee75f11f0a9b1b4006daec2381bb497e02f6.tar.bz2
better-report-e50cee75f11f0a9b1b4006daec2381bb497e02f6.zip
Rewrote EVERYTHING! WAY better now, but only works with ZOOM reports atm
-rwxr-xr-xmain.odin420
-rwxr-xr-xparts/end.html43
-rwxr-xr-xparts/start2.html6
3 files changed, 306 insertions, 163 deletions
diff --git a/main.odin b/main.odin
index c25ee7e..1a4f4ef 100755
--- a/main.odin
+++ b/main.odin
@@ -5,10 +5,33 @@ import "core:os"
import "core:path/filepath"
import "core:strings"
+/*
+TODO: Fix sort direction toggle behavior
+TODO: If submitted path is a directory, traverse the tree to find all .CSVs
+TODO: Move info that's all the same throughout the table into the info header
+*/
+
+VERBOSE :: true
+
PART_ONE :: #load("parts/start.html", string)
PART_TWO :: #load("parts/start2.html", string)
PART_END :: #load("parts/end.html", string)
+Info_Line :: struct {
+ field : string,
+ entry : string,
+}
+
+Report :: struct {
+ title : string,
+ info_lines : []Info_Line,
+ header : []string,
+ table : [][]string,
+ column_count : int,
+ row_count : int,
+ tc_column_index : int,
+}
+
main :: proc() {
input_file_name : string
@@ -29,180 +52,287 @@ main :: proc() {
input_file_name = os.args[1]
}
+ input_file_info, error := os.stat(input_file_name)
+ if error == os.ERROR_NONE {
+ parsed, ok_parse := parse(input_file_info.fullpath)
+ if !ok_parse {
+ fmt.printf("Parse failed: {}\n", input_file_info.fullpath)
+ }
+ output_name := fmt.aprintf("{}_Knekt.html", input_file_info.fullpath[:len(input_file_info.fullpath)-4])
+ render(parsed, output_name)
+ } else {
+ fmt.printf("Skipped: {}\n", input_file_name)
+ }
- // Reading CSV file
+ free_all(context.temp_allocator)
+}
+
- data, ok := os.read_entire_file(input_file_name, context.allocator)
+parse :: proc(path : string, device : Device = .UNSET) -> (Report, bool) {
+ device := device
+ output : Report = {}
+ data, ok := os.read_entire_file(path, context.temp_allocator)
if !ok {
- fmt.printf("ERROR: Could not read file: {}\n", os.args[1])
- return
+ fmt.printf("ERROR: Could not read file: {}\n", path)
+ return {}, false
}
- defer delete(data, context.allocator)
+ file_info, _ := os.lstat(path, context.temp_allocator)
- total_output: string = PART_ONE // Page title
- total_output = append(total_output, fmt.aprintf("{} - Lydrapport", input_file_name[len(input_file_name)-17:len(input_file_name)-11]))
- total_output = append(total_output, PART_TWO)
- it := string(data)
- // First we do a pass to find out at what column the channels start and end,
- // and to find out how many channels we actually use at maximum.
- // This will let us compress the channel columns to only what is needed.
- tracks_start := 0
- tracks_end := 0
- max_tracks := 0
- line_index := 0
+ // STAGE 1 --------------------------------------------------------------
+ // First, we detect what kind of sound report this is
+ line_number := 0
+ it := string(data)
for line in strings.split_lines_iterator(&it) {
- tracks_seen_this_line := 0
- for element, e in strings.split(line, ",") {
- if element == "Trk 1" {
- tracks_start = e
- fmt.printf("Tracks start at: {}\n", tracks_start)
- }
- if element == "Notes" {
- tracks_end = e-1
- fmt.printf("Tracks end at: {}\n", tracks_end)
- }
- if e >= tracks_start && tracks_end >= e { // If there's anything in the range of where tracks are, count it
- if element != "" do tracks_seen_this_line += 1
- }
+ if (device!=.UNSET) { break }
+ if line == "\"SOUND REPORT\"," {
+ device = .ZOOM
+ if VERBOSE do fmt.printf("Detected ZOOM from quotes and comma on line index {}\n", line_number)
+ }
+ if line == "\"ZOOM F8\"," {
+ device = .ZOOM
+ if VERBOSE do fmt.printf("Detected ZOOM from \"ZOOM F8\" on line index {}\n", line_number)
+ }
+ if line == "SOUND REPORT" {
+ device = .SOUND_DEVICES
+ if VERBOSE do fmt.printf("Detected SOUND_DEVICES from unquoted SOUND REPORT line index {}\n", line_number)
}
- // Update the max
- if tracks_seen_this_line > max_tracks {
- max_tracks = tracks_seen_this_line
- fmt.printf("Highest track count so far found on line {}, with {} tracks.\n", line_index, max_tracks)
- }
- line_index += 1
+ line_number += 1
}
- potensial_tracks := tracks_end+1 - tracks_start
- unused_tracks := potensial_tracks - max_tracks
-
- // Now we output the HTML.
- it = string(data)
- line_index = 0
- blank_lines := 0
- state : states = .TITLE
- time_in_state := 0
- info_reading_to_field := true
- for line in strings.split_lines_iterator(&it) {
- //fmt.printf("state.{} line_index:{} t:{} blank_lines:{} : \t", state, line_index, time_in_state, blank_lines)
- there_was_content_in_line := false
+
+
+
+
+ // STAGE 2 --------------------------------------------------------------
+ // Measuring content for allocation
+ if device == .ZOOM {
+ output.column_count = 21 // Ugly magic number, could be fucked by firmware update
+ output.info_lines = make([]Info_Line, 2, context.temp_allocator)
- // Writing the headers and section stuff.
- #partial switch state {
- case .HEADER:
- if time_in_state == 1 do total_output = append(total_output, " <table>\n <thead>\n <tr class=\"header-tr\">\n")
-
- case .BODY:
- total_output = append(total_output, " <tr>\n")
+ output.row_count = strings.count(string(data), "\n") - 7 // Ugly magic number, could be fucked by firmware update
+ output.table = make([][]string, output.row_count, context.temp_allocator)
+ output.header = make([]string, output.column_count, context.temp_allocator)
+ for &row in output.table {
+ row = make([]string, output.column_count, context.temp_allocator)
}
-
- columns_skipped := 0
- for element, e in strings.split(line, ",") {
- if len(element)> 0 do there_was_content_in_line = true
- if len(element)> 0 do blank_lines = 0
- // Filling the row of data
- switch state {
- case .TITLE:
- //fmt.printf("{} T ", element)
- case .INFO:
- //fmt.printf("{} I ", element)
-
- if len(element)>0 {
- if info_reading_to_field {
- total_output = append(total_output, fmt.aprintf(" <p><b>{}</b>", element))
- } else {
- total_output = append(total_output, fmt.aprintf(" {}</p>\n", element[1:len(element)-1]))
- }
- info_reading_to_field = !info_reading_to_field
- }
-
- case .HEADER:
- //fmt.printf("{} H ", element)
- if time_in_state == 1 {
- if e == 4 { // Magic number to select the default sorting column
- total_output = append(total_output, fmt.aprintf(" <th class=\"current-sort\">{}</th>\n", element))
- } else if e <= tracks_start+max_tracks-1 || e > tracks_end {
- total_output = append(total_output, fmt.aprintf(" <th>{}</th>\n", element))
+ }
+
+
+
+ // STAGE 3 --------------------------------------------------------------
+ // Filling with data
+
+ if VERBOSE do fmt.printf("Struct before main parse:\n%#v\n", output)
+
+ first_channel_index := -1
+ last_channel_index := -1
+
+ stage : Stages = .TITLE
+ #partial switch device {
+ case .SOUND_DEVICES:
+ fmt.printf("ERROR parsing {}: Sound Devices reports are not yet supported in this version.\n", path)
+ return {}, false
+
+
+ case .ZOOM:
+ fmt.printf("Parsing [{}] as ZOOM report.\n", path)
+
+ // Getting title
+ if file_info.name[:8] == "F8n Pro_" {
+ output.title = file_info.name[8:len(file_info.name)-4]
+ } else if file_info.name[:4] == "F8n_" { // I don't own one, so I don't know if this is what the F8n does
+ output.title = file_info.name[4:len(file_info.name)-4]
+ } else if file_info.name[:3] == "F8_" { // TODO: Verify this is what the original F8 does
+ output.title = file_info.name[4:len(file_info.name)-4]
+ }
+ fmt.printf("Title: {}\n", output.title)
+
+ line_index := 0
+ info_line_index := 0
+ body_line_index := 0
+ it := string(data)
+ for line in strings.split_lines_iterator(&it) {
+ switch stage {
+ case .TITLE:
+ if line_index == 1 { // Ugly magic number, could be fucked by firmware update
+ stage = .INFO
}
- }
-
- case .BODY:
- //fmt.printf("{} B ", element)
- if e == tracks_end+1 {
- for _ in 0..<(columns_skipped - unused_tracks) { // Add padding so note at end ligns up
- total_output = append(total_output, " <td></td>\n")
+
+ case .INFO:
+ if line == "" {
+ stage = .HEADER
+ continue
}
- }
- if element != "" {
-
- if e >= tracks_start {
- total_output = append(total_output, fmt.aprintf(" <td>{}</td>\n", element[1:len(element)-1]))
- /*} else if e == 0 { // Making filename a link to the file
- total_output = append(total_output, fmt.aprintf(" <td><a href=\"{}\">{}</a></td>\n", element, element))*/
- } else {
- total_output = append(total_output, fmt.aprintf(" <td>{}</td>\n", element))
+ line_elements := strings.split(line, ",")
+ if VERBOSE do fmt.printf(".INFO {}: {}\n", line_index, line_elements)
+ field_raw := line_elements[0]
+ entry_raw := line_elements[1]
+ field := line_elements[0][1:len(field_raw)-1]
+ entry := line_elements[1][1:len(entry_raw)-1]
+ output.info_lines[info_line_index].field = field
+ output.info_lines[info_line_index].entry = entry
+ info_line_index += 1
+
+ case .HEADER:
+ if VERBOSE do fmt.printf(".HEADER {}:", line_index)
+ // to skip empty entry after trailing comma we do a silly slice
+ for element, e in strings.split(line, ",")[:output.column_count] {
+ if VERBOSE do fmt.printf(" {}", element)
+ output.header[e] = element[1:len(element)-1]
+
+ if element[:4] == "\"Tr " {
+ if first_channel_index==-1 do first_channel_index = e
+ last_channel_index = e
+ output.header[e] = fmt.aprintf("Trk {}", e-first_channel_index+1, allocator=context.temp_allocator)
+ }
+ if element == "\"Start TC\"" {
+ output.tc_column_index = e
+ }
}
+ if VERBOSE do fmt.printf("\n")
+ stage = .BODY
- } else {
- columns_skipped += 1
- }
+ case .BODY:
+ if VERBOSE do fmt.printf("first_channel_index: {}\n", first_channel_index)
+ if VERBOSE do fmt.printf("last_channel_index: {}\n", last_channel_index)
+ if line == "" do break
+ if VERBOSE do fmt.printf(".BODY {}:", line_index)
+ // to skip empty entry after trailing comma we do a silly slice
+ for element, e in strings.split(line, ",")[:output.column_count] {
+ if VERBOSE do fmt.printf(" {}", element)
+ output.table[body_line_index][e] = element[1:len(element)-1]
+ }
+ if VERBOSE do fmt.printf("\n")
+ body_line_index += 1
+ }
+ line_index += 1
+ }
+ }
+
+
+
+
+ // STAGE 4 --------------------------------------------------------------
+ // Cleanup!
+ if VERBOSE do fmt.printf("Struct before cleanup:\n%#v\n", output)
+
+ // Stacking tracks to the left
+ for &line, l in output.table {
+ stacking_index := first_channel_index
+ for &field, f in line[first_channel_index:last_channel_index+1] {
+ if field != "" {
+ line[stacking_index] = field
+ stacking_index += 1
}
}
- // Advancing state machine and finishing off divs n stuff
- switch state {
- case .TITLE:
- if line_index==1 {
- state = .INFO
- time_in_state = 0
- }
- case .INFO:
- if blank_lines > 1 {
- total_output = append(total_output, " </div>\n </div>\n")
- state = .HEADER
- time_in_state = 0
- }
- case .HEADER:
- if blank_lines > 0 {
- state = .BODY
- time_in_state = 0
- total_output = append(total_output, " </tr>\n </thead>\n <tbody>\n")
- }
- case .BODY:
- total_output = append(total_output, " </tr>\n")
+ for &field, f in line[stacking_index:last_channel_index+1] {
+ field = ""
}
- if there_was_content_in_line {
- blank_lines = 0
- } else {
- blank_lines += 1
+ }
+
+ // Cleaning out unused columns
+ touched := make([]bool, output.column_count, context.temp_allocator)
+ for line, l in output.table {
+ for field, f in line {
+ if touched[f] do continue
+ if field != "" {
+ touched[f] = true
+ }
+ }
+ }
+ for &line, l in output.table {
+ stacking_index := 0
+ for &field, f in line {
+ if touched[f] {
+ line[stacking_index] = field
+ stacking_index += 1
+ }
+ }
+ for &field, f in line[stacking_index:] {
+ field = ""
}
- //fmt.printf("\n")
- line_index += 1
- time_in_state += 1
}
- total_output = append(total_output, PART_END)
+ stacking_index := 0
+ for &field, f in output.header {
+ if touched[f] {
+ output.header[stacking_index] = field
+ stacking_index += 1
+ }
+ }
+ for &field, f in output.header[stacking_index:] {
+ field = ""
+ }
+ new_column_count := 0
+ for b in touched {
+ if b do new_column_count+=1
+ }
+ output.column_count = new_column_count
+
+ if VERBOSE do fmt.printf("Struct before output:\n%#v\n", output)
+
+ return output, true
+}
+
- output_file_name := fmt.aprintf("{}_Knekt.html", os.args[1][:len(os.args[1])-4])
- os.remove(output_file_name)
- output_file_handle, _ := os.open(output_file_name, os.O_CREATE | os.O_RDWR, 0o777)
- os.write_string(output_file_handle, total_output)
- os.close(output_file_handle)
+render :: proc(report : Report, path : string) {
+ // Now we output the HTML.
+
+ builder := strings.builder_make(context.temp_allocator)
+
+ strings.write_string(&builder, PART_ONE)
+ strings.write_string(&builder, report.title)
+ strings.write_string(&builder, " - Lydrapport")
+ strings.write_string(&builder, PART_TWO)
+
+
+ for line, l in report.info_lines {
+ strings.write_string(&builder, " <p><b>")
+ strings.write_string(&builder, line.field)
+ strings.write_string(&builder, "</b> ")
+ strings.write_string(&builder, line.entry)
+ strings.write_string(&builder, "</p>\n")
+ }
+ strings.write_string(&builder, " </div>\n </div>\n <table>\n <thead>\n <tr class=\"header-tr\">\n")
+
+ for field, f in report.header[:report.column_count] {
+ if f != report.tc_column_index {
+ strings.write_string(&builder, " <th>")
+ } else {
+ strings.write_string(&builder, " <th class=\"current-sort\">")
+ }
+ strings.write_string(&builder, field)
+ strings.write_string(&builder, "</th>\n")
+ }
- fmt.printf("Wrote file: {}\n", output_file_name)
+ strings.write_string(&builder, " </tr>\n </thead>\n <tbody>\n")
- /*fmt.printf("\n\n\nTOTAL OUTPUT:\n\n\n")
- fmt.println(total_output)*/
+ for line, l in report.table {
+ strings.write_string(&builder, " <tr>\n")
+ for field, f in line[:report.column_count] {
+ strings.write_string(&builder, " <td>")
+ strings.write_string(&builder, field)
+ strings.write_string(&builder, "</td>\n")
+
+ }
+ strings.write_string(&builder, " </tr>\n")
+ }
+
+ strings.write_string(&builder, PART_END)
+
+ output_text := strings.to_string(builder)
+ os.write_entire_file(path, transmute([]u8)output_text)
}
-append :: proc(input, to_add: string) -> string {
- output := strings.concatenate({input, to_add})
- //delete(to_add)
- //delete(input)
- return output
+
+Device :: enum {
+ UNSET,
+ ZOOM,
+ SOUND_DEVICES,
}
-states :: enum {
+Stages :: enum {
TITLE,
INFO,
HEADER,
diff --git a/parts/end.html b/parts/end.html
index f964978..cc51027 100755
--- a/parts/end.html
+++ b/parts/end.html
@@ -1,33 +1,46 @@
- </tbody>
- </table>
- </div>
+ </tbody>
+ </table>
</div>
-
</body>
<script type="text/javascript">
const getCellValue = (tr, idx) => tr.children[idx].innerText || tr.children[idx].textContent;
-
+ prevColumnSorted = document.getElementsByClassName("current-sort")[0];
+
const comparer = (idx, asc) => (a, b) => ((v1, v2) =>
v1 !== '' && v2 !== '' && !isNaN(v1) && !isNaN(v2) ? v1 - v2 : v1.toString().localeCompare(v2)
)(getCellValue(asc ? a : b, idx), getCellValue(asc ? b : a, idx));
-
+
function doSort(th) {
- console.log("Doing sort to: ", th.textContent, th)
+ console.log("Doing sort to: ", th.textContent, th);
+
const table = th.closest('table');
- Array.from(table.querySelector('tbody').querySelectorAll('tr:nth-child(n)'))
- .sort(comparer(Array.from(th.parentNode.children).indexOf(th), this.asc = !this.asc))
- .forEach(tr => table.querySelector('tbody').appendChild(tr) );
+ const tbody = table.querySelector('tbody');
+ const columnIndex = Array.from(th.parentNode.children).indexOf(th);
+
+ // Toggle sort direction
+ if (prevColumnSorted == th) {
+ this.asc = !this.asc;
+ } else {
+ this.asc = true;
+ }
+
+ // Get all rows, sort them, then reappend to tbody
+ const rows = Array.from(tbody.querySelectorAll('tr'));
+ const sortedRows = rows.sort(comparer(columnIndex, this.asc));
+
+ sortedRows.forEach(row => tbody.appendChild(row));
+ prevColumnSorted = th;
}
- // do the work...
- document.querySelectorAll('th').forEach(th => th.addEventListener('click', (() => {
+
+ // Event listeners and initial sort
+ document.querySelectorAll('th').forEach(th => th.addEventListener('click', () => {
document.getElementsByClassName("current-sort")[0].classList.remove("current-sort");
th.classList.add("current-sort");
doSort(th);
- })));
-
+ }));
+
doSort(document.getElementsByClassName("current-sort")[0]);
-
</script>
</html>
diff --git a/parts/start2.html b/parts/start2.html
index bd0021e..bd43709 100755
--- a/parts/start2.html
+++ b/parts/start2.html
@@ -98,6 +98,6 @@
</svg>
</a>
<h1 style="width:max-content;margin:auto;">Lydrapport</h1>
- <h6 style="width:max-content;margin:auto;">Version 0.2</h6>
- </div>
- <div class="info-section"> \ No newline at end of file
+ <h6 style="width:max-content;margin:auto;">Version 0.5</h6>
+ </div>
+ <div class="info-section">