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]]
7 # Attrib: Steven Baltakatei Sandoval. (2024-07-17). reboil.com
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
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()
22 if [[ "$#" -gt 1 ]]; then die
"FATAL:Too many arguments ($#):$*"; fi;
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.";
33 # Desc: Consumes stdin; outputs as stdout lines
34 # Input: stdin (consumes)
35 # Output: stdout (newline delimited)
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)
41 # Attrib: Steven Baltakatei Sandoval (2024-01-29). reboil.com
42 local input_stdin output
;
45 if [[ -p /dev
/stdin
]]; then
46 input_stdin
="$(cat -)" ||
{
47 echo "FATAL:Error reading stdin." 1>&2; return 1; };
52 # Store as output array elements
54 if [[ -n $input_stdin ]]; then
55 while read -r line
; do
57 done < <(printf "%s\n" "$input_stdin") ||
{
58 echo "FATAL:Error parsing stdin."; return 1; };
62 printf "%s\n" "${output[@]}";
65 }; # read stdin to stdout lines
66 get_path_fork_level
() {
67 # Desc: Get fork level from two paths
68 # Input: arg1 str path
70 # Output: stdout int fork level
75 # Squeeze multiple slashes and remove trailing slashes
76 path1
="$(echo "$path1" | tr -s '/' | sed 's:/*$::' )";
77 path2
="$(echo "$path2" | tr -s '/' | sed 's:/*$::' )";
79 # Check for mixed absolute/relative paths
80 if [[ "$path1" =~ ^
/ ]] && [[ "$path2" =~ ^
/ ]]; then
83 path1
="$(echo "$path1" | sed -e 's:^/::' )";
84 path2
="$(echo "$path2" | sed -e 's:^/::' )";
85 elif [[ ! "$path1" =~ ^
/ ]] && [[ ! "$path2" =~ ^
/ ]]; then
88 declare -p path1 path2 flag_root
;
89 echo "FATAL:Mixed relative and absolute paths not supported." 1>&2;
93 # Save path as arrays with `/` as element delimiter
95 read -ra parts1
<<< "$path1";
96 read -ra parts2
<<< "$path2";
98 # Get fork level by counting identical path elements from rootside
100 for (( i
=0; i
<${#parts1[@]} && i
<${#parts2[@]}; i
++ )); do
101 if [[ "${parts1[i]}" != "${parts2[i]}" ]]; then break; fi;
106 #declare -p path1 path2 flag_root parts1 parts2 fork_level; # debug
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
116 local prune_level
="$2";
118 # Check for absolute or relative path
119 if [[ "$path" =~ ^
/ ]]; then
122 path
="$(echo "$path" | sed -e 's:^/::' )";
127 # Save path as array with `/` as element delimiter
129 read -ra parts
<<< "$path";
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]}/";
137 # Trim trailing `/` delimiter
138 pruned_path
=$
(echo "$pruned_path" |
sed 's:/*$::');
140 # Restore initial / if appropriate
141 if [[ "$flag_root" == "true" ]] && [[ "$prune_level" -eq 0 ]]; then
142 pruned_path
=/"$pruned_path";
147 #declare -p path prune_level parts pruned_path && printf "========\n"; # debug
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
162 while read -r line
; do
163 # Check for mixed absolute/relative paths.
164 if [[ $n -le 0 ]] && [[ "$line" =~ ^
/ ]]; then
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;
174 # Squeeze multiple slashes and remove trailing slashes
175 line
="$(echo "$line" | tr -s '/' | sed 's:/*$::' )";
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;
183 #declare -p flag_root level; # debug
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()
195 while read -r line
; do
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";
203 # Reject trailing spaces.
204 re_ts
=' $'; # match trailing space
205 if [[ "$line" =~
$re_ts ]]; then
206 die
"FATAL:Trailing spaces not allowed:$line";
209 # Replace some chars with HTML-style codes
210 ## replace ampersand & with & # must be first
211 ## replace double quote " with "
212 ## replace single quote ' with '
217 <<< "$line" )" || { echo "FATAL
:Error running
sed.
"; };
218 printf "%s
\n" "$line";
220 echo "FATAL
:Error reading stdin.
" 1>&2; return 1; };
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()
231 while read -r line; do
232 #yell "$n:Processing line
:$line"; # debug
233 # Advance input lines
237 #declare -p lprev lcurr lnext; # debug
239 # Update hierarchy tracker states
240 lprev_hier="$lcurr_hier";
241 lcurr_hier="$lnext_hier";
242 lnext_hier="$
(echo "$lnext" | get_path_hierarchy_level
)";
244 # Skip first iteration
245 if [[ "$n" -eq 0 ]]; then
246 # yell "$n:DEBUG
:Skipping first iteration.
"; # debug
248 #printf -- "----\n" 1>&2; # debug
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")";
255 # Count relative ups needed (`../`)
256 relups_next="$
((lcurr_hier
- fork_level_next
+ 1))";
257 relups_prev="$
((lcurr_hier
- fork_level_prev
+ 1))";
259 # Initialize Next and Prev links with relative ups to fork.
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
264 for (( i=0; i<relups_prev; i++ )); do link_prev+="..
/"; done;
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")";
270 # Print navigation link wikicode
271 if [[ -z "$lprev" ]]; then
272 printf "[[%s|Next
]], [[..
/|Up
]]\n" "$link_next";
273 elif [[ -n "$lnext" ]]; 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";
278 echo "FATAL
:Here be dragons.
" 1>&2;
281 #declare -p n line 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 #printf "====================\n" # debug
286 done < <(read_stdin; printf "\n"; ); # read stdin plus one more blank line
287 }; # Generate wikicode from validated subpage lines
289 read_input "$@
" | validate_subpage_list | generate_wikicode;