5cdf74a92eed4ecef14ba1b778b839e3a6722426
[BK-2020-03.git] / user / mw_create_subpage_navlinks.sh
1 #!/bin/bash
2 # Desc: Generates Mediawiki subpage navigation links
3 # Input: file text file with list of chapters
4 # stdin text with list of chapters
5 # Output: [[../Chapter 4|Next]], [[../Chapter 2|Previous]], [[../|Up]]
6 # Version: 0.1.0
7 # Attrib: Steven Baltakatei Sandoval. (2024-07-17). reboil.com
8 # License: GPLv3+
9
10 yell() { echo "$0: $*" >&2; } # print script path and all args to stderr
11 die() { yell "$*"; exit 111; } # same as yell() but non-zero exit status
12 must() { "$@" || die "cannot $*"; } # runs args as command, reports args if command fails
13 read_input() {
14 # Note: May accept specified file or stdin, but not both
15 # Input: arg input text (newline delimited)
16 # stdin input text (newline delimited)
17 # Output: stdout output text (newline delimited)
18 # Depends: BK-2020-03 read_stdin (v0.1.1), yell(), die()
19
20 # Parse args
21 ## Only 1 argument.
22 if [[ "$#" -gt 1 ]]; then die "FATAL:Too many arguments ($#):$*"; fi;
23 ## File specified.
24 if [[ "$#" -eq 1 ]] && [[ -f "$1" ]] && [[ ! -p /dev/stdin ]]; then
25 while read -r line; do
26 printf "%s\n" "$line";
27 done < "$1" && return 0; fi;
28 if [[ "$#" -eq 0 ]] && [[ -p /dev/stdin ]]; then
29 read_stdin && return 0; fi;
30 die "FATAL:Unknown error.";
31 };
32 read_stdin() {
33 # Desc: Consumes stdin; outputs as stdout lines
34 # Input: stdin (consumes)
35 # Output: stdout (newline delimited)
36 # return 0 stdin read
37 # return 1 stdin not present
38 # Example: printf "foo\nbar\n" | read_stdin
39 # Depends: GNU bash (version 5.1.16), GNU Coreutils 8.32 (cat)
40 # Version: 0.1.1
41 # Attrib: Steven Baltakatei Sandoval (2024-01-29). reboil.com
42 local input_stdin output;
43
44 # Store stdin
45 if [[ -p /dev/stdin ]]; then
46 input_stdin="$(cat -)" || {
47 echo "FATAL:Error reading stdin." 1>&2; return 1; };
48 else
49 return 1;
50 fi;
51
52 # Store as output array elements
53 ## Read in stdin
54 if [[ -n $input_stdin ]]; then
55 while read -r line; do
56 output+=("$line");
57 done < <(printf "%s\n" "$input_stdin") || {
58 echo "FATAL:Error parsing stdin."; return 1; };
59 fi;
60
61 # Print to stdout
62 printf "%s\n" "${output[@]}";
63
64 return 0;
65 }; # read stdin to stdout lines
66 get_path_fork_level() {
67 # Desc: Get fork level from two paths
68 # Input: arg1 str path
69 # arg2 str path
70 # Output: stdout int fork level
71 # Version: 0.0.1
72 local path1="$1";
73 local path2="$2";
74
75 # Squeeze multiple slashes and remove trailing slashes
76 path1="$(echo "$path1" | tr -s '/' | sed 's:/*$::' )";
77 path2="$(echo "$path2" | tr -s '/' | sed 's:/*$::' )";
78
79 # Check for mixed absolute/relative paths
80 if [[ "$path1" =~ ^/ ]] && [[ "$path2" =~ ^/ ]]; then
81 flag_root=true;
82 # Remove initial /
83 path1="$(echo "$path1" | sed -e 's:^/::' )";
84 path2="$(echo "$path2" | sed -e 's:^/::' )";
85 elif [[ ! "$path1" =~ ^/ ]] && [[ ! "$path2" =~ ^/ ]]; then
86 flag_root=false;
87 else
88 declare -p path1 path2 flag_root;
89 echo "FATAL:Mixed relative and absolute paths not supported." 1>&2;
90 return 1;
91 fi;
92
93 # Save path as arrays with `/` as element delimiter
94 local IFS='/';
95 read -ra parts1 <<< "$path1";
96 read -ra parts2 <<< "$path2";
97
98 # Get fork level by counting identical path elements from rootside
99 local fork_level=0;
100 for (( i=0; i<${#parts1[@]} && i<${#parts2[@]}; i++ )); do
101 if [[ "${parts1[i]}" != "${parts2[i]}" ]]; then break; fi;
102 ((fork_level++));
103 done;
104
105 echo "$fork_level";
106 #declare -p path1 path2 flag_root parts1 parts2 fork_level; # debug
107 return 0;
108 }; # Get fork level int from two paths
109 prune_path_rootside() {
110 # Desc: Prunes a path from the root-side to a specified prune level.
111 # Input: arg1 str path
112 # arg2 int prune level (0-indexed)
113 # Depends: GNU sed 4.8
114 # Version: 0.0.1
115 local path="$1";
116 local prune_level="$2";
117
118 # Check for absolute or relative path
119 if [[ "$path" =~ ^/ ]]; then
120 flag_root=true;
121 # Remove initial /
122 path="$(echo "$path" | sed -e 's:^/::' )";
123 else
124 flag_root=false;
125 fi;
126
127 # Save path as array with `/` as element delimiter
128 local IFS='/';
129 read -ra parts <<< "$path";
130
131 # Assemble pruned path from prune_level
132 local pruned_path="";
133 for (( i=prune_level; i<${#parts[@]}; i++ )); do
134 pruned_path+="${parts[i]}/";
135 done;
136
137 # Trim trailing `/` delimiter
138 pruned_path=$(echo "$pruned_path" | sed 's:/*$::');
139
140 # Restore initial / if appropriate
141 if [[ "$flag_root" == "true" ]] && [[ "$prune_level" -eq 0 ]]; then
142 pruned_path=/"$pruned_path";
143 fi;
144
145 # Output pruned path
146 echo "$pruned_path";
147 #declare -p path prune_level parts pruned_path && printf "========\n"; # debug
148 return 0;
149 }; # prune path rootside to int specified level
150 get_path_hierarchy_level() {
151 # Desc: Outputs hierarchy level of input paths
152 # Example: $ cat lines.txt | get_path_hierarchy_level
153 # Input: stdin str lines with /-delimited paths
154 # Output: stdout int hierarchy level of each path
155 # Version: 0.0.1
156
157 local line level;
158 local flag_root;
159 local -a output;
160
161 n=0;
162 while read -r line; do
163 # Check for mixed absolute/relative paths.
164 if [[ $n -le 0 ]] && [[ "$line" =~ ^/ ]]; then
165 flag_root=true;
166 else
167 flag_root=false;
168 fi;
169 if { [[ "$flag_root" == "true" ]] && [[ ! "$line" =~ ^/ ]]; } || \
170 { [[ "$flag_root" == "false" ]] && [[ "$line" =~ ^/ ]]; } then
171 echo "FATAL:Mixed relative and absolute paths not supported." 1>&2; return 1;
172 fi;
173
174 # Squeeze multiple slashes and remove trailing slashes
175 line="$(echo "$line" | tr -s '/' | sed 's:/*$::' )";
176
177 # Count the number of slashes to determine hierarchy level
178 level="$(echo "$line" | awk -F'/' '{print NF-1}' )";
179 if [[ "$flag_root" == "true" ]]; then ((level--)); fi;
180
181 # Append to output
182 output+=("$level");
183 #declare -p flag_root level; # debug
184 ((n++));
185 done;
186 # Print output
187 printf "%s\n" "${output[@]}";
188 }; # return hierarchy level of lines as integers
189 validate_subpage_list() {
190 # Desc: Check for illegal characters in subpage titles
191 # Input: stdin unvalidated subpage list
192 # Output: stdout validated subpage list
193 # Depends: BK-2020-03 read_stdin(), yell(), die()
194 # GNU sed v4.8
195 while read -r line; do
196
197 # Reject chars illegal in Mediawiki page titles.
198 re_illegal='[][><|}{#_]'; # match illegal page names chars #, <, >, [, ], _, {, |, }
199 if [[ "$line" =~ $re_illegal ]]; then
200 die "FATAL:Illegal char. Not allowed: #, <, >, [, ], _, {, |, }:$line";
201 fi;
202
203 # Reject trailing spaces.
204 re_ts=' $'; # match trailing space
205 if [[ "$line" =~ $re_ts ]]; then
206 die "FATAL:Trailing spaces not allowed:$line";
207 fi;
208
209 # Replace some chars with HTML-style codes
210 ## replace ampersand & with &#38 # must be first
211 ## replace double quote " with &#34
212 ## replace single quote ' with &#39
213 line="$(sed \
214 -e 's/&/\&#38;/g' \
215 -e 's/"/\&#34;/g' \
216 -e "s/'/\&#39;/g" \
217 <<< "$line" )" || { echo "FATAL:Error running sed."; };
218 printf "%s\n" "$line";
219 done || {
220 echo "FATAL:Error reading stdin." 1>&2; return 1; };
221 };
222 generate_wikicode() {
223 # Desc: Generates navigational link wikicode for subpages
224 # Input: stdin validated subpage list
225 # Output: stdout wikicode
226 # Depends: get_path_fork_level()
227 # prune_path_rootside()
228 # get_path_hierarchy_level()
229
230 n=0;
231 while read -r line; do
232 #yell "$n:Processing line:$line"; # debug
233 # Advance input lines
234 lprev="$lcurr";
235 lcurr="$lnext";
236 lnext="$line";
237 #declare -p lprev lcurr lnext; # debug
238
239 # Update hierarchy tracker states
240 lprev_hier="$lcurr_hier";
241 lcurr_hier="$lnext_hier";
242 lnext_hier="$(echo "$lnext" | get_path_hierarchy_level)";
243
244 # Skip first iteration
245 if [[ "$n" -eq 0 ]]; then
246 # yell "$n:DEBUG:Skipping first iteration."; # debug
247 ((n++));
248 #printf -- "----\n" 1>&2; # debug
249 continue; fi;
250
251 # Get path fork levels
252 fork_level_next="$(get_path_fork_level "$lcurr" "$lnext")";
253 fork_level_prev="$(get_path_fork_level "$lcurr" "$lprev")";
254
255 # Count relative ups needed (`../`)
256 relups_next="$((lcurr_hier - fork_level_next + 1))";
257 relups_prev="$((lcurr_hier - fork_level_prev + 1))";
258
259 # Initialize Next and Prev links with relative ups to fork.
260 link_next="";
261 for (( i=0; i<relups_next; i++ )); do link_next+="../"; done;
262 if [[ "$relups_next" -eq 0 ]]; then link_next+="/"; fi; # handle new subpage path dive
263 link_prev="";
264 for (( i=0; i<relups_prev; i++ )); do link_prev+="../"; done;
265
266 # Append branchs from fork to Next and Prev targets
267 link_next+="$(prune_path_rootside "$lnext" "$fork_level_next")";
268 link_prev+="$(prune_path_rootside "$lprev" "$fork_level_prev")";
269
270 # Print navigation link wikicode
271 if [[ -z "$lprev" ]]; then
272 printf "[[%s|Next]], [[../|Up]]\n" "$link_next";
273 elif [[ -n "$lprev" ]]; then
274 printf "[[%s|Next]], [[%s|Previous]], [[../|Up]]\n" "$link_next" "$link_prev";
275 elif [[ -z "$lnext" ]]; then
276 printf "[[%s|Previous]], [[../|Up]]\n" "$link_prev";
277 else
278 echo "FATAL:Here be dragons." 1>&2;
279 fi;
280
281 #declare -p n lprev lcurr lnext lprev_hier lcurr_hier lnext_hier; # debug
282 #declare -p fork_level_next fork_level_prev relups_next relups_prev; # debug
283 #declare -p link_next link_prev; # debug
284 ((n++));
285 done < <(read_stdin); # read stdin plus one more blank line
286 }; # Generate wikicode from validated subpage lines
287 main() {
288 read_input "$@" | validate_subpage_list | generate_wikicode;
289 }; # main program
290
291 main "$@";