Utensil's Zettelkasten-style forest of evergreen notes on math and tech.
utensil.tngl.sh/forest/
1#!/bin/bash
2# set -e
3
4OUT_DIR=output/forest
5# XSL_FILE=$OUT_DIR/default.xsl
6XSL_FILE=$OUT_DIR/uts-forest.xsl
7
8function convert_xml_to_html() {
9 local xml_file=$1
10 # Extract NOTE_ID from path: $OUT_DIR/NOTE_ID/index.xml
11 local note_id=$(basename $(dirname "$xml_file"))
12 local html_file="$OUT_DIR/$note_id/index.html"
13
14 # echo "[convert_xml_to_html] xml_file: $xml_file"
15 # echo "[convert_xml_to_html] note_id: $note_id"
16 # echo "[convert_xml_to_html] html_file: $html_file"
17 # echo "[convert_xml_to_html] xsltproc -v -o \"$html_file\" $XSL_FILE \"$xml_file\""
18
19 # bunx xslt3 -s:"$xml_file" -xsl:output/uts-forest.xsl -o:"$html_file"
20 # -v
21 xsltproc --path $OUT_DIR -o "$html_file" $XSL_FILE "$xml_file"
22 # echo "[convert_xml_to_html] Finished xsltproc for $html_file"
23 # head -n 20 "$html_file"
24}
25# AGENT-NOTE: Updated for new forest path structure (input/output under $OUT_DIR/NOTE_ID/)
26
27function backup_html_files() {
28 mkdir -p $OUT_DIR/.bak
29 # Copy each $OUT_DIR/NOTE_ID/index.html to $OUT_DIR/.bak/NOTE_ID/index.html
30 for html_file in $OUT_DIR/*/index.html; do
31 note_id=$(basename $(dirname "$html_file"))
32 mkdir -p "$OUT_DIR/.bak/$note_id"
33 cp -f "$html_file" "$OUT_DIR/.bak/$note_id/index.html" 2>/dev/null || true
34 done
35}
36# AGENT-NOTE: Updated backup logic for forest structure
37
38function check_html_changes() {
39 local note_id=$1
40 local html_file="$OUT_DIR/$note_id/index.html"
41 local bak_file="$OUT_DIR/.bak/$note_id/index.html"
42 [ ! -f "$bak_file" ] || ! cmp -s "$html_file" "$bak_file"
43}
44# AGENT-NOTE: Updated for forest structure (compare $OUT_DIR/NOTE_ID/index.html)
45
46function sample_based_change_detected() {
47 local xml_files=("$@")
48 local total_files=${#xml_files[@]}
49 local sample_size=3
50 backup_html_files
51 local changes_detected=false
52 sample_pids=()
53 sample_note_ids=()
54 for ((i = 0; i < sample_size && i < total_files; i++)); do
55 local xml_file="${xml_files[i]}"
56 local note_id=$(basename $(dirname "$xml_file"))
57 convert_xml_to_html "$xml_file" &
58 sample_pids+=($!)
59 sample_note_ids+=("$note_id")
60 done
61 for pid in "${sample_pids[@]}"; do
62 wait "$pid"
63 done
64 cmp_pids=()
65 for note_id in "${sample_note_ids[@]}"; do
66 (
67 if check_html_changes "$note_id"; then
68 touch "$OUT_DIR/.bak/$note_id/changed"
69 fi
70 ) &
71 cmp_pids+=($!)
72 done
73 for pid in "${cmp_pids[@]}"; do
74 wait "$pid"
75 done
76 for note_id in "${sample_note_ids[@]}"; do
77 if [ -f "$OUT_DIR/.bak/$note_id/changed" ]; then
78 changes_detected=true
79 break
80 fi
81 done
82 find $OUT_DIR/.bak -name changed -delete
83 $changes_detected && return 0 || return 1
84}
85# AGENT-NOTE: Updated for forest structure (sample and change detection)
86
87function update_progress_bar() {
88 local i=$1
89 local progress=$2
90 local progress_step=$3
91 local new_progress=$((i / progress_step))
92 while [ $progress -lt $new_progress ] && [ $progress -lt 20 ]; do
93 echo -n "█"
94 ((progress++))
95 done
96 return $progress
97}
98
99function parallel_convert_xml_files() {
100 local convert_all=$1
101 # echo "[parallel_convert_xml_files] convert_all: $convert_all"
102 local max_jobs=$2
103 shift 2
104 local xml_files=("$@")
105 local total_files=${#xml_files[@]}
106 local progress=0
107 local progress_step=$((total_files / 20)) # 5% intervals
108 [ $progress_step -eq 0 ] && progress_step=1
109 echo -n "Progress: "
110 local updated_file_list="$OUT_DIR/.updated_files"
111 : > "$updated_file_list"
112 pids=()
113 for ((i = 0; i < total_files; i++)); do
114 local xml_file="${xml_files[i]}"
115 # echo "[parallel_convert_xml_files] Processing: $xml_file"
116 (
117 local note_id=$(basename $(dirname "$xml_file"))
118 local html_file="$OUT_DIR/$note_id/index.html"
119 local bak_file="$OUT_DIR/.bak/$note_id/index.html"
120 local do_convert=0
121
122 if [ "$convert_all" = true ] || [ -n "$XSL_CHANGED" ] || [ "$changes_detected" = true ]; then
123 do_convert=1
124 elif [ ! -f "$html_file" ]; then
125 do_convert=1
126 elif [ "$XSL_FILE" -nt "$html_file" ]; then
127 do_convert=1
128 fi
129
130 # echo "[parallel_convert_xml_files] note_id: $note_id do_convert: $do_convert"
131
132 if [ $do_convert -eq 1 ]; then
133 convert_xml_to_html "$xml_file"
134 echo "$note_id" >> "$updated_file_list"
135 exit 0
136 fi
137 exit 1
138 ) &
139 pids+=($!)
140 if [ "${#pids[@]}" -ge "$max_jobs" ]; then
141 wait "${pids[0]}"
142 pids=("${pids[@]:1}")
143 fi
144
145 update_progress_bar $i $progress $progress_step
146 progress=$?
147 done
148 for pid in "${pids[@]}"; do
149 wait "$pid"
150 done
151 echo # New line after progress bar
152}
153# AGENT-NOTE: Updated for forest structure (parallel conversion and progress)
154
155
156function detect_max_jobs() {
157 # Cross-platform CPU core detection
158 local num_cores=1
159 if [ -n "$CI" ]; then
160 num_cores=2
161 elif [ -f /proc/cpuinfo ]; then
162 num_cores=$(grep -c ^processor /proc/cpuinfo)
163 elif [ "$(uname)" = "Darwin" ]; then
164 num_cores=$(sysctl -n hw.ncpu 2>/dev/null || echo 1)
165 else
166 # Default to 1 core if we can't detect
167 num_cores=1
168 fi
169 local max_jobs=$((num_cores > 2 ? num_cores - 2 : 2))
170 echo "$max_jobs"
171}
172
173function convert_xml_files() {
174 local convert_all=$1
175 local start_time=$(date +%s)
176
177 # Get all XML files in $OUT_DIR/*/index.xml
178 local xml_files=($OUT_DIR/*/index.xml)
179 local total_files=${#xml_files[@]}
180 # echo "[convert_xml_files] Found $total_files XML files:"
181 # for xml_file in "${xml_files[@]}"; do
182 # echo "[convert_xml_files] $xml_file"
183 # done
184 local max_jobs=$(detect_max_jobs)
185 echo "Max jobs: $max_jobs"
186
187 if [ "$convert_all" = true ]; then
188 if [ -n "$XSL_CHANGED" ]; then
189 if ! sample_based_change_detected "${xml_files[@]}"; then
190 echo "⏩ XSL changes don't affect HTML output, skipping conversion"
191 return 0
192 fi
193 fi
194 echo "Converting all ${total_files} XML files..."
195 fi
196
197 parallel_convert_xml_files "$convert_all" "$max_jobs" "${xml_files[@]}"
198
199 local updated_file_list="$OUT_DIR/.updated_files"
200 local updated_count=0
201 if [ -f "$updated_file_list" ]; then
202 updated_count=$(wc -l < "$updated_file_list")
203 rm -f "$updated_file_list"
204 fi
205
206 local end_time=$(date +%s)
207 local duration=$((end_time - start_time))
208 echo "📝 Updated $updated_count HTML file(s) in ${duration}s"
209
210}
211# AGENT-NOTE: Updated for forest structure (xml listing and updated files)
212