Commit | Line | Data |
---|---|---|
752ae10d SBS |
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]] | |
4f51959a SBS |
6 | # Version: 0.1.0 |
7 | # Attrib: Steven Baltakatei Sandoval. (2024-07-17). reboil.com | |
752ae10d SBS |
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 | |
4f51959a SBS |
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 | |
752ae10d SBS |
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 & # must be first | |
211 | ## replace double quote " with " | |
212 | ## replace single quote ' with ' | |
213 | line="$(sed \ | |
214 | -e 's/&/\&/g' \ | |
215 | -e 's/"/\"/g' \ | |
216 | -e "s/'/\'/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() { | |
4f51959a | 223 | # Desc: Generates navigational link wikicode for subpages |
752ae10d SBS |
224 | # Input: stdin validated subpage list |
225 | # Output: stdout wikicode | |
4f51959a SBS |
226 | # Depends: get_path_fork_level() |
227 | # prune_path_rootside() | |
228 | # get_path_hierarchy_level() | |
752ae10d SBS |
229 | |
230 | n=0; | |
231 | while read -r line; do | |
232 | #yell "$n:Processing line:$line"; # debug | |
4f51959a | 233 | # Advance input lines |
752ae10d SBS |
234 | lprev="$lcurr"; |
235 | lcurr="$lnext"; | |
236 | lnext="$line"; | |
237 | #declare -p lprev lcurr lnext; # debug | |
238 | ||
4f51959a SBS |
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 | ||
752ae10d SBS |
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 | ||
4f51959a SBS |
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")"; | |
752ae10d | 254 | |
4f51959a SBS |
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 | |
752ae10d SBS |
287 | main() { |
288 | read_input "$@" | validate_subpage_list | generate_wikicode; | |
289 | }; # main program | |
290 | ||
291 | main "$@"; |