Update file headers year copyright
[wmaker-crm.git] / script / generate-txt-from-texi.sh
blobdcb3976070e851c9f27c32c0112efd5e1c581bd2
1 #!/bin/sh
2 ###########################################################################
4 # Window Maker window manager
6 # Copyright (c) 2014-2015 Christophe CURIS
7 # Copyright (c) 2015-2019 Window Maker Team
9 # This program is free software; you can redistribute it and/or modify
10 # it under the terms of the GNU General Public License as published by
11 # the Free Software Foundation; either version 2 of the License, or
12 # (at your option) any later version.
14 # This program is distributed in the hope that it will be useful,
15 # but WITHOUT ANY WARRANTY; without even the implied warranty of
16 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 # GNU General Public License for more details.
19 # You should have received a copy of the GNU General Public License along
20 # with this program; if not, write to the Free Software Foundation, Inc.,
21 # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
23 ###########################################################################
25 # generate-txt-from-texi.sh:
26 # generate a plain text file from a texinfo documentation
28 # The goal is to achieve a result similar to:
29 # texi2any --plaintext --no-split <filename>
31 # The reason for this script is that we do not want to add a strict
32 # dependancy on the 'makeinfo' tool suite (which has its own dependancies)
34 # There is also the problem that we use is to generate some documentations
35 # that should be available before the 'configure' script have been run
36 # (in the 'autogen.sh' script) because some of these documentation provide
37 # information for running the 'configure' script; We distribute these
38 # generated docs so normal user won't have the problem, but people that
39 # want to build from the Git repository.
41 # This script is not a reference for Texinfo syntax, so if you modified the
42 # texi source, you should really consider running texi2any at least once to
43 # make sure everything is still ok.
45 ###########################################################################
47 # Despite trying to be close to the texi2any output, this script has a few
48 # known differences:
50 # - texi2any does not generate a proper title header, it satisfy itself
51 # with a rudimentary single line; this script generates a better looking
52 # header with all the information provided
54 # - the table of content contains the line number for the section to ease
55 # navigation contrary to texi2any that satisfy itself with a simplist toc
57 # - the paragraphs are properly justified, contrary to texi2any which only
58 # flush left them in text outputs
60 # - There are 2 blank lines instead of 1 before chapter/section to make
61 # them stand out more
63 # - when making cross-references, generate a decent-looking string with
64 # the target line number, instead of the crypting "*Note:" label (inherited
65 # from "info" format) that looks out of place
67 # - the line length is set to 76 instead of 72
69 # - there are some difference in what characters are added when a style is
70 # used for a string (@emph, @file, @sc, ...) because it assumes the text
71 # will be read in a plain text tool with no special "smart" highlighting
73 # - it does not check that the syntax is valid; there are some simple
74 # checks but it may misbehave, it does not replace a quality check with
75 # texi2any
77 # - not all commands are implemented, some because they were not needed
78 # so far, probably a few because they would be too complex to implement
81 # There are a few limitations due to Texinfo being a cheesy format:
83 # - the @node should be followed immediately by its @chapter/@section
84 # command, otherwise you may have mismatch on the line number if you make a
85 # cross-reference to that @node
87 ###########################################################################
89 # Please note that this script is writen in sh+awk on purpose: this script
90 # is gonna be run on the machine of the person who is trying to compile
91 # WindowMaker, and as such we cannot be sure to find any scripting language
92 # in a known version and that works (python/ruby/tcl/perl/php/you-name-it).
94 # So for portability, we stick to the same sh+awk constraint as Autotools
95 # to limit the problem, see for example:
96 # http://www.gnu.org/savannah-checkouts/gnu/autoconf/manual/autoconf-2.69/html_node/Portable-Shell.html
98 ###########################################################################
100 # Report an error on stderr and exit with status 1 to tell make could not work
101 arg_error() {
102 echo "$0: $@" >&2
103 exit 1
106 # print help and exit with success status
107 print_help() {
108 echo "$0: convert a Texinfo file into a plain text file"
109 echo "Usage: $0 [options...] file.texi"
110 echo "valid options are:"
111 echo " -Dvar=val : set variable 'var' to value 'val'"
112 echo " -d date : use 'date' for @today instead of current date"
113 echo " -e email : set email address in variable 'emailsupport'"
114 echo " -v version : version of the project"
115 echo " -o file : name of text file to create"
116 exit 0
119 today_date="`LANG=C date '+%d %B %Y' | sed -e 's,^0,,' `"
121 # Extract command line arguments
122 while [ $# -gt 0 ]; do
123 case $1 in
125 -D*)
126 echo "$1" | grep '^-D[a-zA-Z][a-zA-Z]*=' > /dev/null || arg_error "syntax error for '$1', expected -Dname=value"
127 var_defs="$var_defs
128 `echo "$1" | sed -e 's/^-D/ variable["/ ; s/=/"] = "/ ; s/$/";/' `"
132 shift
133 today_date="$1"
137 shift
138 var_defs="$var_defs
139 variable[\"emailsupport\"] = \"@email{`echo "$1" | sed -e 's/@/@@/g' `}\";"
142 -h|-help|--help) print_help ;;
145 shift
146 output_file="$1"
150 shift
151 project_version="$1"
154 -*) arg_error "unknow option '$1'" ;;
157 [ "x$input_file" = "x" ] || arg_error "only 1 input file can be specified, not \"$input_file\" and \"$1\""
158 input_file="$1"
160 esac
161 shift
162 done
164 var_defs="$var_defs
165 variable[\"today\"] = \"$today_date\";"
167 # Check consistency of command-line
168 [ "x$input_file" != "x" ] || arg_error "no input texi file given"
169 [ "x$output_file" != "x" ] || arg_error "no output file given"
171 ###########################################################################
172 # The script works in 2 passes, in the first pass we generate an almost
173 # complete text file in the temporary file $temp_file, it also generates
174 # the table of content in $toc_file as a sed script.
175 # The second pass generate the $output_file from that $temp_file and the
176 # $toc_file
177 ###########################################################################
179 # Create the temp file in the current directory
180 temp_file="`echo "$input_file" | sed -e 's,^.*/\([^/]*\)$,\1, ; s,\.[^.]*$,,' `.tmp"
181 toc_file="`echo "$temp_file" | sed -e 's,\.[^.]*$,,' `.toc"
182 xref_file="`echo "$temp_file" | sed -e 's,\.[^.]*$,,' `.xrf"
184 # Run awk for 1st pass, but if it fails stop now without deleting temp files
185 awk '
186 # Stop processing everything, print the message for user and return error code
187 # to tell "make" to not go further
188 function report_error(message) {
189 print "Error: " message > "/dev/stderr";
191 # When we call "exit", the block "END" is still called, we falsely set
192 # this variable to skip a spurious second error message
193 bye_marker_found = 1;
195 # Code 1 is used when the script is invoked with incorrect arguments
196 # Code 2 is used by awk to report problems
197 exit 3;
200 # Conditionals for @ifXXX and @ifnotXXX commands
201 # stored in a stack to allow embedding conditionals inside other conditionals
202 # the global variable "cond_state" contains the current condition (0 or 1)
203 function start_conditional(name, value, local_i) {
204 cond_level++;
205 cond_names[cond_level] = name;
206 cond_value[cond_level] = value;
207 cond_state = value;
208 for (local_i = 1; local_i < cond_level; local_i++) {
209 cond_state = cond_state && cond_value[local_i];
213 function end_conditional(name, local_i) {
214 cond_level--;
215 cond_state = 1;
216 for (local_i = 1; local_i < cond_level; local_i++) {
217 cond_state = cond_state && cond_value[local_i];
221 # Texinfo Variables
222 # the texinfo standard allows to have variables set with @set and used
223 # with @value; they can also be defined from command-line (-D)
224 # they are stored in the global array "variable[name]"
225 function set_variable(line, local_idx, local_name, local_value) {
226 gsub(/^[ \t]+/, "", line);
227 local_idx = match(line, /[ \t]/);
228 if (local_idx > 0) {
229 local_name = substr(line, 1, local_idx - 1);
230 local_value = substr(line, local_idx + 1);
231 gsub(/^[ \t]+/, "", local_value);
232 } else {
233 local_name = line;
234 local_value = "";
236 variable[ local_name ] = local_value;
239 # Write a single line to the output
240 function write_line(line) {
241 if (!cond_state) { return; }
243 if (redirect_out == "no") {
244 print line;
245 line_number++;
247 } else if (redirect_out == "copyright") {
248 copyright_lines[copyright_count++] = line;
250 } else {
251 report_error("redirect output mode \"" redirect_out "\" is not supported (line " NR ")");
255 # Paragraph modes
256 # the current mode for paragraph handling is a Stack to allow embedding
257 # modes inside other modes
258 # the global variable "par_mode" contains the active mode
259 function par_mode_push(mode, local_i) {
260 par_mode_count++;
261 par_mode_save_previous[par_mode_count] = par_mode;
262 par_mode_save_length[par_mode_count] = line_length;
263 par_mode_save_prefix[par_mode_count] = line_prefix;
264 par_mode_save_justify[par_mode_count] = par_justify;
265 par_mode_save_itemmark[par_mode_count] = item_list_mark;
266 par_mode = mode;
268 # Check for quality of output
269 if (length(line_prefix) + 25 > line_length) {
270 print "Warning: too many paragraph modes imbricated at line " NR " for " mode > "/dev/stderr";
271 line_length = length(line_prefix) + 25;
275 function par_mode_pop(mode, local_i) {
276 if ((par_mode != mode) || (par_mode_count <= 0)) {
277 report_error("found @end " mode " at line " NR " but not in @" mode " (current state is @" par_mode ")");
279 par_mode = par_mode_save_previous[par_mode_count];
280 line_length = par_mode_save_length[par_mode_count];
281 line_prefix = par_mode_save_prefix[par_mode_count];
282 par_justify = par_mode_save_justify[par_mode_count];
283 item_list_mark = par_mode_save_itemmark[par_mode_count];
284 par_mode_count--;
287 # Discard all the lines in the file until the specified "@end" is found on a line by itself
288 function discard_block(name, local_start_line) {
289 local_start_line = NR;
290 while (1) {
291 if (getline == 0) { report_error("end of file reached while searching \"@end " name "\", started at line " local_start_line); }
292 if ($0 == "@end " name) { break; }
296 # Title Page generation
297 function generate_title_page() {
298 if (!cond_state) { return; }
300 if (par_nb_words > 0) {
301 generate_paragraph();
302 write_line(gen_underline(0, 76));
305 # Title page start with 5 blank lines so the "title" coming after will
306 # stand out a little bit
307 write_line("");
308 write_line("");
309 write_line("");
310 par_mode_push("titlepage");
311 line_prefix = " ";
312 line_length = 76 - 4;
315 function generate_title_page_title(title, local_array, local_count, local_i) {
316 if (!cond_state) { return; }
318 if (par_mode != "titlepage") {
319 report_error("command @title used outside @titlepage, at line " NR);
321 generate_paragraph();
323 # Title deserves more space
324 write_line("");
325 write_line("");
327 # Split long title
328 if (length(title) < 60) {
329 local_count = 1;
330 local_array[1] = title;
331 } else {
332 local_count = int((length(title) + 59 ) / 60);
333 sub_length = int((length(title) + local_count - 1) / local_count);
335 local_count = 0;
336 while (length(title) > 0) {
337 if (length(title) > sub_length) {
338 # Cut at first space before the length
339 local_i = sub_length + 1;
340 while (local_i > 0) {
341 if (substr(title, local_i, 1) == " ") { break; }
342 local_i--;
344 if (local_i == 0) {
345 # Can not break first word, break at first possible place
346 local_i = index(title, " ");
347 if (local_i == 0) { local_i = length(title) + 1; }
349 } else {
350 local_i = length(title) + 1;
353 local_count++;
354 local_array[local_count] = substr(title, 1, local_i - 1);
356 title = substr(title, local_i + 1);
360 # Center the title
361 for (local_i = 1; local_i <= local_count; local_i++) {
362 write_line(gen_underline(-1, int((76 - length(local_array[local_i])) / 2)) local_array[local_i]);
365 write_line("");
366 write_line("");
369 function generate_title_page_subtitle(title, local_array, local_count, local_i) {
370 if (!cond_state) { return; }
372 if (par_mode != "titlepage") {
373 report_error("command @subtitle used outside @titlepage, at line " NR);
375 generate_paragraph();
377 # Split long lines
378 if (length(title) < 65) {
379 local_count = 1;
380 local_array[1] = title;
381 } else {
382 local_count = int((length(title) + 64) / 65);
383 sub_length = int((length(title) + local_count - 1) / local_count);
385 local_count = 0;
386 while (length(title) > 0) {
387 if (length(title) > sub_length) {
388 # Cut at first space before the length
389 local_i = sub_length + 1;
390 while (local_i > 0) {
391 if (substr(title, local_i, 1) == " ") { break; }
392 local_i--;
394 if (local_i == 0) {
395 # Can not break first word, break at first possible place
396 local_i = index(title, " ");
397 if (local_i == 0) { local_i = length(title) + 1; }
399 } else {
400 local_i = length(title) + 1;
403 local_count++;
404 local_array[local_count] = substr(title, 1, local_i - 1);
406 title = substr(title, local_i + 1);
410 # Center the title
411 for (local_i = 1; local_i <= local_count; local_i++) {
412 write_line(gen_underline(-1, int((76 - length(local_array[local_i]) - 4) / 2)) "~ " local_array[local_i] " ~");
416 # Generate separation line to simulate page breaks in plain text file
417 function generate_page_break() {
418 if (!cond_state) { return; }
420 generate_paragraph();
421 if (par_mode = "titlepage") {
422 write_line("");
424 write_line("");
425 write_line(gen_underline(0, 76));
426 par_indent = 1;
429 # Handle chapter and section declaration
430 # take care of the automatic numbering and to put the line in the table of
431 # content file, then generate the underlined line in output
432 function new_section(level, title, is_numbered, local_i, local_line) {
433 if (!cond_state) { return; }
435 # Dump the current paragraph now
436 generate_paragraph();
438 # Update the counters
439 if (is_numbered) {
440 section[level]++;
441 for (local_i = level + 1; local_i <= 4; local_i++) {
442 section[local_i] = 0;
446 # Generate the line to be displayed
447 if (is_numbered) {
448 local_line = section[1];
449 for (local_i = 2; local_i <= level; local_i++) {
450 local_line = local_line "." section[local_i];
452 local_line = local_line " " title;
453 } else {
454 local_line = title;
457 # Add the entry to the ToC
458 toc_count++;
459 toc_entry_level[toc_count] = level;
460 toc_entry_name[toc_count] = local_line;
461 for (local_i = 1; local_i < level; local_i++) {
462 toc_entry_name[toc_count] = " " toc_entry_name[toc_count];
464 toc_entry_line[toc_count] = line_number + 3;
466 # Print the section description
467 write_line("");
468 write_line("");
469 write_line(local_line);
470 write_line(gen_underline(level, length(local_line)));
471 par_indent = 0;
474 # Do not generate anything for Node command, but keep the line information so
475 # the nodes can be cross-referenced
476 function new_node(args, local_nb, local_arr, local_i) {
477 if (!cond_state) { return; }
479 # Dump the current paragraph now
480 generate_paragraph();
482 # The command takes many arguments, separate them because we care only for the 1st
483 local_nb = split(args, local_arr, ",");
484 if ((local_nb < 1) || (local_nb > 4)) {
485 report_error("bad number of argument " local_nb " for @node at line " NR);
487 gsub(/^[ \t]+/, "", local_arr[1]);
488 gsub(/[ \t]*$/, "", local_arr[1]);
489 if (local_arr[1] == "") {
490 report_error("missing node name for @node at line " NR);
493 # Consistency check
494 if (node_address[local_arr[1]] != "") {
495 report_error("node \"" local_arr[1] "\" is redefined at line " NR ", previous definition at line " node_defline[local_arr[1]]);
498 # Add a +3 offset to compensate for the position of the real location that will be the
499 # chapter/section that should be following
500 node_address[local_arr[1]] = line_number + 3;
501 node_defline[local_arr[1]] = NR;
504 # List of Items
505 function start_item_list(mark, type, default_mark) {
506 par_mode_push(type);
507 list_is_first_item = 1;
508 list_item_wants_sepline = 0;
509 par_indent = 1;
510 if (line_prefix == "") {
511 # First level of enumeration get one mode indentation space
512 line_prefix = " ";
513 } else {
514 line_prefix = line_prefix " ";
516 if (mark == "") {
517 item_list_mark = default_mark;
518 } else {
519 item_list_mark = execute_commands(mark);
521 write_line("");
524 # One item in a Table
525 function generate_item_in_table(line) {
526 if (line !~ /^[ \t]*@itemx?[ \t]/) {
527 report_error("bas usage for @item inside a @table, should be at start of line and followed by its value");
530 generate_paragraph();
531 if (list_item_wants_sepline && !list_is_first_item) {
532 write_line("");
535 # Apply the global table style to this item
536 gsub(/^[ \t]*@itemx?[ \t]*/, "", line);
537 line = execute_commands(item_list_mark "{" line "}");
539 # Cancel the indentation added for the 2nd column for that line
540 line = substr(line_prefix, 1, length(line_prefix)-5) line;
541 write_line(line);
543 list_item_wants_sepline = 0;
546 # Generate Underline string with the specified length
547 function gen_underline(id, len, local) {
548 if (id == -1) { local = " "; } else
549 if (id == 1) { local = "**********"; } else
550 if (id == 2) { local = "=========="; } else
551 if (id == 3) { local = "----------"; } else
552 if (id == 4) { local = ".........."; }
553 else { local = "~~~~~~~~~~"; }
554 while (length(local) < len) {
555 local = local local;
557 return substr(local, 1, len);
560 # Generate text for an URL link
561 function generate_url_reference(args, local_nb, local_arr) {
562 local_nb = split(args, local_arr, ",");
563 if (local_nb == 1) {
564 return local_arr[1];
566 } else if (local_nb == 2) {
567 return execute_commands(local_arr[2]) " (" local_arr[1] ")";
569 } else if (local_nb == 3) {
570 return execute_commands(local_arr[3]);
572 } else {
573 report_error("bad number of argument " local_nb " for @uref at line " NR);
577 # Generate text for a Cross-Reference into the document
578 function generate_cross_reference(args, cmd, local_nb, local_arr, local_i) {
579 local_nb = split(args, local_arr, ",");
580 if ((local_nb < 1) || (local_nb > 5)) {
581 report_error("bad number of argument " local_nb " for @" cmd " at line " NR);
584 local_arr[1] = execute_commands(local_arr[1]);
585 for (local_i = 1; local_i <= local_nb; local_i++) {
586 gsub(/^[ \t]+/, "", local_arr[local_i]);
587 gsub(/[ \t]*$/, "", local_arr[local_i]);
590 if (local_arr[1] == "") {
591 report_error("no node name specified in @" cmd " at line " NR);
594 if (local_arr[4] != "") {
595 # If it is a link to an external documentation, do not create an xref_table entry
596 local_i = "section [" local_arr[1] "]";
597 if (local_arr[5] != "") {
598 local_i = local_i " in " execute_commands("@cite{" local_arr[5] "}");
599 } else {
600 local_i = local_i " from " execute_commands("@file{" local_arr[4] "}");
602 return local_i;
605 # Build the string to be displayed
606 if (local_arr[3] != "") {
607 local_i = "section [" execute_commands(local_arr[3]) "]";
608 } else {
609 local_i = "section [" local_arr[1] "]";
612 xref_idcount++;
613 xref_table[xref_idcount] = local_arr[1];
614 xref_defline[xref_idcount] = NR;
615 local_i = local_i sprintf(", at line @x%02X@", xref_idcount);
616 return local_i;
619 # Generate a line with the name of an author
620 # note, we assume the name(s) always fit on a line
621 function generate_author_line(name, local_offset, local_attach_to_par) {
622 if (!cond_state) { return; }
624 local_attach_to_par = (par_nb_words > 0);
626 generate_paragraph();
628 if (par_mode == "titlepage") {
629 name = "-- " name " --";
630 local_offset = int((76 - length(name)) / 2);
631 if (local_offset < 2) { local_offset = 2; }
632 write_line("");
633 write_line(gen_underline(-1, local_offset) name);
635 } else if (par_mode == "quotation") {
636 name = "-- " name;
637 local_offset = int((line_length - length(line_prefix) - length(name)) * 2/3);
638 if (local_offset < length(line_prefix) + 2) { local_offset = length(line_prefix) + 2; }
639 if (!local_attach_to_par) { write_line(""); }
640 write_line(line_prefix gen_underline(-1, local_offset) name);
642 } else {
643 report_error("command @author used in an inappropriate mode (" par_mode ") at line " NR);
647 # Add the specified line to the curren paragraph being built, do not print anything yet
648 function add_text_to_paragraph(line) {
649 nb = split(line, words, /[ \t]+/);
650 for (i = 1; i <= nb; i++) {
651 if (words[i] != "") {
652 par_word[par_nb_words++] = words[i];
657 # Print the paragraph from all the lines read so far
658 function generate_paragraph( local_prefix, local_line, local_length,
659 idx_word_start, idx_word_end, local_i) {
660 if (par_nb_words <= 0) { return; }
662 local_line = line_prefix;
664 if (par_mode == "list") {
665 if (list_item_wants_sepline && !list_is_first_item) {
666 write_line("");
668 list_is_first_item = 0;
669 list_item_wants_sepline = 0;
670 if (!par_indent) {
671 local_prefix = item_list_mark " ";
672 while (length(local_prefix) < 5) { local_prefix = " " local_prefix; }
673 local_line = substr(local_line, 1, length(local_line) - 5) local_prefix;
676 } else if (par_mode == "enum") {
677 if (list_item_wants_sepline && !list_is_first_item) {
678 write_line("");
680 list_is_first_item = 0;
681 list_item_wants_sepline = 0;
682 if (!par_indent) {
683 local_prefix = " " item_list_mark ". ";
684 local_line = substr(local_line, 1, length(local_line) - 5) local_prefix;
686 # Increment the enumeration counter for the next item now
687 if (item_list_mark + 0 == item_list_mark) {
688 item_list_mark++;
689 } else {
690 local_i = index("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz", item_list_mark);
691 if (local_i == 0) {
692 report_error("value \"" item_list_mark "\" is not supported for enumerated list - invalid @enumerate argument or list too long?");
694 item_list_mark = substr("BCDEFGHIJKLMNOPQRSTUVWXYZ!bcdefghijklmnopqrstuvwxyz!", local_i, 1);
698 } else if (par_mode == "table") {
699 if (list_item_wants_sepline && !list_is_first_item) {
700 write_line("");
702 list_is_first_item = 0;
703 list_item_wants_sepline = 0;
705 } else if (par_mode == "titlepage") {
706 write_line("");
708 } else if (par_mode == "par") {
709 write_line("");
710 if (par_indent) {
711 local_line = local_line " ";
714 } else if (par_mode == "quotation") {
715 write_line("");
716 # There is no extra indentation of paragraphs in this mode
718 } else {
719 report_error("paragraph mode \"" par_mode "\" is not supported in generate_paragraph (line " NR ")");
722 # Split the paragraph in lines
723 idx_word_start = 0;
724 while (idx_word_start < par_nb_words) {
725 # First word is always printed, this makes sure that words too long for a line will
726 # always be printed, very likely on a line by themselfs
727 idx_word_end = idx_word_start;
728 local_length = length(local_line) + length(par_word[idx_word_start]);
729 idx_word_start++;
731 # See how many word we can fit on the line
732 while (idx_word_end < par_nb_words - 1) {
733 if (local_length + 1 + length(par_word[idx_word_end + 1]) > line_length) { break; }
734 idx_word_end++;
735 local_length = local_length + 1 + length(par_word[idx_word_end]);
738 # Put all those words on the current line with the appropriate justification
739 if (par_justify == "right") {
740 local_line = local_line gen_underline(-1, line_length - local_length) par_word[idx_word_start - 1];
741 while (idx_word_start <= idx_word_end) {
742 local_line = local_line " " par_word[idx_word_start++];
744 } else {
745 if ((par_justify == "left") || (idx_word_end == par_nb_words - 1) ||
746 (local_length >= line_length) || (idx_word_end < idx_word_start)) {
747 local_line = local_line par_word[idx_word_start - 1];
748 while (idx_word_start <= idx_word_end) {
749 local_line = local_line " " par_word[idx_word_start++];
751 } else {
752 # We calculate the ideal size of a space (as a real number) which would
753 # make all the words perfectly fill the line, the formula being
754 # ideal size = 1 + needed_extra_spaces / number_of_spaces_in_line
755 ideal_space_length = 1 + (line_length - local_length) / (idx_word_end - idx_word_start + 1);
756 count_spaces = 0;
757 for (local_i = idx_word_start; local_i <= idx_word_end; local_i++) {
758 count_spaces = count_spaces + ideal_space_length;
759 word_space[local_i] = gen_underline(-1, int(count_spaces + 0.5));
760 count_spaces = count_spaces - length(word_space[local_i]);
763 local_line = local_line par_word[idx_word_start - 1];
764 while (idx_word_start <= idx_word_end) {
765 local_line = local_line word_space[idx_word_start] par_word[idx_word_start++];
770 write_line(local_line);
772 # Reset for next line
773 local_line = line_prefix;
775 par_nb_words = 0;
776 par_indent = 1;
779 # Replace commands by text in the line, return the result
780 function execute_commands(line, replaced_line, command) {
781 replaced_line = "";
782 while (1) {
783 idx = match(line, /@([a-zA-Z]+|.)/);
784 if (idx == 0) { break; }
786 # Separate the command and its arguments from the rest of the line
787 replaced_line = replaced_line substr(line, 1, idx - 1);
788 command = substr(line, idx + 1, RLENGTH - 1);
789 line = substr(line, idx + RLENGTH);
791 if (line ~ /^\{/) {
792 # Command has argument(s), extract them
793 brace_count = 0;
794 for (i = 1; i <= length(line); i++) {
795 if (substr(line, i, 1) == "{") {
796 brace_count++;
798 if (substr(line, i, 1) == "}") {
799 brace_count--;
800 if (brace_count == 0) { break; }
803 if (brace_count != 0) {
804 report_error("closing brace not found for command \"@" command "\", at line " NR);
807 cmdargs = substr(line, 2, i-2);
808 line = substr(line, i + 1);
810 } else {
811 # Command does not have arguments, discard the spaces used to separate it
812 # from the next text
813 cmdargs = "";
814 sub(/^[ \t]+/, "", line);
817 # Commands generating "special" characters #################################
818 if (command == "@") {
819 replaced_line = replaced_line "@";
821 } else if (command == "bullet") {
822 replaced_line = replaced_line "*";
824 } else if (command == "copyright") {
825 replaced_line = replaced_line "(c)";
827 } else if (command == "minus") {
828 replaced_line = replaced_line "-";
830 } else if (command == "registeredsymbol") {
831 replaced_line = replaced_line "(r)";
833 } else if (command == "today") {
834 # Make sure the date will be in english (we use "C" because it not certain
835 # that the English locale is enabled on the machine of the user)
836 replaced_line = replaced_line variable["today"];
838 # Commands to display text in a special style ##############################
839 } else if (command == "asis") {
840 line = cmdargs line;
842 } else if (command == "b") { # bold
843 line = "*" cmdargs "*" line;
845 } else if (command == "emph") {
846 line = cmdargs line;
848 } else if ((command == "code") ||
849 (command == "command") ||
850 (command == "env") ||
851 (command == "option") ||
852 (command == "var")) {
853 # Should be in fixed-spacing font; printed with single-quotes
854 line = "'\''" cmdargs "'\''" line;
856 } else if (command == "i") { # italic
857 line = "_" cmdargs "_" line;
859 } else if (command == "email") {
860 line = "<" cmdargs ">" line;
862 } else if (command == "file") {
863 line = "\"" cmdargs "\"" line;
865 } else if (command == "key") {
866 line = "<" cmdargs ">" line;
868 } else if (command == "r") { # roman font
869 line = cmdargs line;
871 } else if (command == "sc") {
872 # Small-Caps, keep as-is in plain text
873 line = cmdargs line;
875 } else if (command == "t") { # typewriter-like
876 line = cmdargs line;
878 # References to other places ###############################################
879 } else if (command == "anchor") {
880 if (anchor_address[cmdargs] != "") {
881 report_error("anchor \"" cmdargs "\" is redefined at line " NR ", previous definition at line " anchor_defline[cmdargs]);
883 # Set a -1 offset to compensate for the anchor being after the actual target
884 anchor_address[cmdargs] = line_number - 1;
885 anchor_defline[cmdargs] = NR;
887 } else if (command == "cite") {
888 # Reference to external document, we cannot do much
889 line = cmdargs line;
891 } else if (command == "pxref") {
892 replaced_line = replaced_line "see " generate_cross_reference(cmdargs, command);
894 } else if (command == "ref") {
895 replaced_line = replaced_line generate_cross_reference(cmdargs, command);
897 } else if (command == "uref") {
898 replaced_line = replaced_line generate_url_reference(cmdargs);
900 } else if (command == "xref") {
901 replaced_line = replaced_line " See " generate_cross_reference(cmdargs, command);
903 # Variable and Conditional commands ########################################
904 } else if (command == "value") {
905 if (variable[cmdargs] == "") {
906 report_error("variable '" cmdargs "' is unknow, for @value at line " NR);
908 line = variable[cmdargs] line;
910 # Miscelleanous commands ###################################################
911 } else if (command == "c") {
912 # Comments: ignore everything to the end of line
913 line = "";
915 } else {
916 report_error("unknow command @" command " at line " NR);
920 return (replaced_line line);
923 # Handle appropriately the "@end xxx"
924 function process_end(line) {
925 if (line == cond_names[cond_level]) {
926 end_conditional(line);
927 return;
929 if (line == "copying") {
930 generate_paragraph();
931 redirect_out = "no";
933 } else if (line == "enumerate") {
934 generate_paragraph();
935 par_mode_pop("enum");
936 par_indent = 1;
938 } else if (line == "example") {
939 generate_paragraph();
940 par_mode_pop("example");
941 par_indent = 1;
943 } else if (line == "flushleft") {
944 generate_paragraph();
945 par_mode_pop(par_mode);
946 par_indent = 1;
948 } else if (line == "flushright") {
949 generate_paragraph();
950 par_mode_pop(par_mode);
951 par_indent = 1;
953 } else if (line == "itemize") {
954 generate_paragraph();
955 par_mode_pop("list");
956 par_indent = 1;
958 } else if (line == "quotation") {
959 generate_paragraph();
960 par_mode_pop("quotation");
961 par_indent = 1;
963 } else if ((line == "table") || (line == "ftable") || (line == "vtable")) {
964 generate_paragraph();
965 par_mode_pop("table");
966 par_indent = 1;
968 } else if (line == "titlepage") {
969 generate_page_break();
970 par_mode_pop("titlepage");
971 par_indent = 0;
973 } else {
974 report_error("unknow command @end " line " at line " NR);
978 BEGIN {
979 # Count the lines generated for the Table of Content
980 line_number = 0;
982 # To perform some basic checks on the file
983 top_was_found = 0;
984 bye_marker_found = 0;
986 # Paragraph generation parameters
987 par_mode_count = 0;
988 par_mode = "par";
989 par_nb_words = 0;
990 par_indent = 1;
991 par_justify = "justify";
992 redirect_out = "no";
993 line_length = 76;
994 line_prefix = "";
996 # To handle conditional code
997 cond_level = 0;
998 cond_state = 1;
1000 # Number of entries in the Table of Content
1001 toc_count = 0;
1002 toc_file = "'"$toc_file"'";
1004 # File to generate for the Cross-Reference tracking
1005 xref_file= "'"$xref_file"'";
1007 # Define a custom variable so it is possible to differentiate between
1008 # texi2any and this script
1009 variable["cctexi2txt"] = "1.0";
1011 # Variables inherited from the command line'"$var_defs"'
1014 # First line is special, we always ignore it
1015 (NR == 1) { next; }
1017 /^[ \t]*@/ {
1018 # Treat the special commands that are supposed to be on a line by themselves
1019 idx = match($0, /^@([a-zA-Z]+)/);
1020 if (idx != 0) {
1021 # Remove the command from current line
1022 command = substr($0, idx + 1, RLENGTH - 1);
1023 line = substr($0, idx + 1 + RLENGTH);
1024 sub(/^[ \t]+/, "", line);
1026 # Commands for structuring the document ####################################
1027 if (command == "chapter") {
1028 new_section(1, execute_commands(line), 1);
1029 next;
1031 } else if (command == "section") {
1032 new_section(2, execute_commands(line), 1);
1033 next;
1035 } else if (command == "subsection") {
1036 new_section(3, execute_commands(line), 1);
1037 next;
1039 } else if (command == "subsubsection") {
1040 new_section(4, execute_commands(line), 1);
1041 next;
1043 } else if (command == "node") {
1044 new_node(execute_commands(line));
1045 next;
1047 } else if (command == "top") {
1048 # This is mandatory for "info" format, but useless for plain text
1049 if (top_was_found > 0) {
1050 report_error("command @top at line " NR " but was already found at line " top_was_found);
1052 top_was_found = NR;
1053 next;
1055 } else if (command == "unnumbered") {
1056 new_section(1, execute_commands(line), 0);
1057 next;
1059 # Commands for content in the Title Page ###################################
1060 } else if (command == "author") {
1061 generate_author_line(execute_commands(line));
1062 next;
1064 } else if (command == "subtitle") {
1065 generate_title_page_subtitle(execute_commands(line));
1066 next;
1068 } else if (command == "title") {
1069 generate_title_page_title(execute_commands(line));
1070 next;
1072 # Commands changing the way paragraph are displayed ########################
1073 } else if (command == "copying") {
1074 generate_paragraph();
1075 redirect_out = "copyright";
1076 copyright_count = 0;
1077 next;
1079 } else if (command == "end") {
1080 process_end(line);
1081 next;
1083 } else if (command == "enumerate") {
1084 if (cond_state) {
1085 generate_paragraph();
1086 start_item_list(line, "enum", "1");
1088 next;
1090 } else if (command == "example") {
1091 if (cond_state) {
1092 generate_paragraph();
1093 write_line("");
1094 par_mode_push("example");
1095 line_prefix = line_prefix " ";
1097 next;
1099 } else if (command == "flushleft") {
1100 if (cond_state) {
1101 generate_paragraph();
1102 par_mode_push(par_mode);
1103 par_justify = "left";
1104 par_indent = 0;
1106 next;
1108 } else if (command == "flushright") {
1109 if (cond_state) {
1110 generate_paragraph();
1111 par_mode_push(par_mode);
1112 par_justify = "right";
1113 par_indent = 0;
1115 next;
1117 } else if (command == "itemize") {
1118 if (cond_state) {
1119 generate_paragraph();
1120 start_item_list(line, "list", "*");
1122 next;
1124 } else if (command == "menu") {
1125 generate_paragraph();
1126 discard_block(command);
1127 next;
1129 } else if (command == "quotation") {
1130 if (cond_state) {
1131 generate_paragraph();
1132 par_mode_push("quotation");
1133 line_prefix = line_prefix " ";
1134 line_length = line_length - 4;
1135 if (line != "") {
1136 add_text_to_paragraph(execute_commands(line));
1137 # We add the ":" to the last word because we count on the function
1138 # "add_text_to_paragraph" to remove the trailing spaces on the line
1139 # first, which would not have happened if we just had appended the ":"
1140 # to the argument in the function call
1141 par_word[par_nb_words - 1] = par_word[par_nb_words - 1] ":";
1142 line = "";
1145 next;
1147 } else if ((command == "table") ||
1148 (command == "ftable") ||
1149 (command == "vtable")) {
1150 # "ftable" and "vtable" are the same as "table" except they are adding automatically
1151 # the item to the appropriate Index (respectively Function and Variable indexes).
1152 # As we do not generate index in the text file, we just treat them identically
1153 if (cond_state) {
1154 generate_paragraph();
1155 par_mode_push("table");
1156 list_is_first_item = 1;
1157 list_item_wants_sepline = 0;
1158 par_indent = 1;
1159 line_prefix = line_prefix " ";
1160 gsub(/[ \t]/, "", line);
1161 if (line !~ /^@[a-z][a-z]*$/) {
1162 report_error("invalid usage of @table, expecting a single style-changing command");
1164 item_list_mark = line;
1165 write_line("");
1167 next;
1169 } else if (command == "titlepage") {
1170 generate_title_page();
1171 next;
1173 # Commands generating text automacitally ###################################
1174 } else if (command == "contents") {
1175 if (cond_state) {
1176 generate_paragraph();
1177 write_line("");
1178 write_line("");
1179 print "@table_of_content@";
1181 next;
1183 } else if (command == "insertcopying") {
1184 if (cond_state) {
1185 generate_paragraph();
1186 # The copying block was already formatted, we just have to print it as-is
1187 for (i = 0; i < copyright_count; i++) {
1188 write_line(copyright_lines[i]);
1191 next;
1193 } else if (command == "page") {
1194 generate_page_break();
1195 next;
1197 } else if (command == "sp") {
1198 if (cond_state) {
1199 generate_paragraph();
1200 while (line > 0) {
1201 write_line("");
1202 line--;
1205 next;
1207 } else if (command == "vskip") {
1208 # Silently ignore, this is just for TeX
1209 if (cond_state) {
1210 generate_paragraph();
1212 next;
1214 # Variable and Conditional commands ########################################
1215 } else if (command == "ifdocbook") { start_conditional(command, 0); line = ""; next;
1216 } else if (command == "ifhtml") { start_conditional(command, 0); line = ""; next;
1217 } else if (command == "ifinfo") { start_conditional(command, 1); line = ""; next; # "for historical compatibility"
1218 } else if (command == "ifplaintext") { start_conditional(command, 1); line = ""; next;
1219 } else if (command == "iftex") { start_conditional(command, 0); line = ""; next;
1220 } else if (command == "ifxml") { start_conditional(command, 0); line = ""; next;
1222 } else if (command == "ifnotdocbook") { start_conditional(command, 1); line = ""; next;
1223 } else if (command == "ifnothtml") { start_conditional(command, 1); line = ""; next;
1224 } else if (command == "ifnotinfo") { start_conditional(command, 0); line = ""; next; # "for historical compatibility"
1225 } else if (command == "ifnotplaintext") { start_conditional(command, 0); line = ""; next;
1226 } else if (command == "ifnottex") { start_conditional(command, 1); line = ""; next;
1227 } else if (command == "ifnotxml") { start_conditional(command, 1); line = ""; next;
1229 } else if (command == "ifclear") { start_conditional(command, (variable[line] == "")); next;
1230 } else if (command == "ifset") { start_conditional(command, (variable[line] != "")); next;
1232 } else if (command == "clear") {
1233 if (cond_state) {
1234 variable[ execute_commands(line) ] = "";
1236 next;
1238 } else if (command == "set") {
1239 if (cond_state) {
1240 set_variable(line);
1242 next;
1244 # Miscelleanous commands ###################################################
1245 } else if (command == "bye") {
1246 # Mark the end of file, we are supposed to ignore everything after
1247 if (cond_state) {
1248 generate_paragraph();
1249 while (getline != 0) { }
1250 bye_marker_found = 1;
1252 next;
1254 } else if (command == "c") {
1255 # Comments: ignore everything to the end of line
1256 next;
1258 } else if (command == "errormsg") {
1259 print "Error: " execute_commands(cmdargs) > "/dev/stderr";
1260 print " (from \"'"$input_file"'\", line " NR ")" > "/dev/stderr";
1261 bye_marker_found = 1;
1262 exit 4;
1264 } else if (command == "finalout") {
1265 # Nothing to do, we are not generating anything in output file about long lines
1266 next;
1268 } else if (command == "ignore") {
1269 # These are multi-lines comments
1270 discard_block(command);
1271 next;
1273 } else if (command == "indent") {
1274 par_indent = 1;
1275 if (line == "") { next; }
1276 $0 = line;
1278 } else if (command == "noindent") {
1279 par_indent = 0;
1280 if (line == "") { next; }
1281 $0 = line;
1283 } else if (command == "setfilename") {
1284 # Should set the output file name automatically
1285 # at current time, we just ignore it
1286 next;
1288 } else if (command == "settitle") {
1289 # This is used for page headers
1290 # in a plain text file, it is useless
1291 next;
1294 # Commands that were not recognised here may be commands that can be used
1295 # anywhere in a line but happenned to be at the beginning of the line this
1296 # time, we do nothing so they will be processed by "execute_commands"
1300 /@item/ {
1301 # We treat @item specially because it may generate more than 1 paragraph
1302 if (!cond_state) { next; }
1304 if (par_mode == "table") {
1305 generate_item_in_table($0);
1306 next;
1307 } else if ((par_mode != "list") && (par_mode != "enum")) {
1308 report_error("found @item at line " NR " but not inside an @itemize");
1311 while (1) {
1312 idx = match($0, /@item/);
1313 if (idx == 0) { break; }
1315 # We generate paragraph with all the text seen so far, which is part of
1316 # the previous item
1317 add_text_to_paragraph(substr($0, 1, idx - 1));
1318 generate_paragraph();
1319 $0 = substr($0, idx + 5);
1321 # When an item is found, we clear "par_ident" to actually place the item
1322 # mark on the next paragragh
1323 par_indent = 0;
1326 # If the item is on a line by itself, stop processing the line to avoid
1327 # skipping lines more than necessary
1328 if (/^[ \t]*$/) { next; }
1331 # Non-empty lines are added to the current paragraph
1333 if (!cond_state) { next; }
1335 if ((par_mode == "list") ||
1336 (par_mode == "enum") ||
1337 (par_mode == "par") ||
1338 (par_mode == "table") ||
1339 (par_mode == "titlepage") ||
1340 (par_mode == "quotation")) {
1341 if (/^[ \t]*$/) {
1342 # Empty lines separate paragraphs
1343 generate_paragraph();
1344 # in list of items, they also tell us that user prefers an aerated list
1345 list_item_wants_sepline = 1;
1346 } else {
1347 add_text_to_paragraph(execute_commands($0));
1350 } else if (par_mode == "example") {
1351 # Line is printed unmodified, not split and not merged, but with an indentation
1352 $0 = line_prefix execute_commands($0);
1353 sub(/[ \t]*$/, "");
1354 write_line($0);
1356 } else {
1357 report_error("paragraph mode \"" par_mode "\" is not supported for line processing (line " NR ")");
1361 END {
1362 if (!bye_marker_found) {
1363 report_error("command \"@bye\" missing at end of file");
1365 if (!top_was_found) {
1366 report_error("command \"@top\" was not found in the file");
1369 # Count the number of lines that the ToC will occupy
1370 # we assume the ToC is at the beginning, so all sections will be shifted
1371 # by this number of lines down
1372 toc_nb_lines = 0;
1373 for (i = 1; i <= toc_count; i++) {
1374 if ((i > 1) && (toc_entry_level[i] == 1)) {
1375 toc_nb_lines++;
1377 toc_nb_lines++;
1380 # Generate the ToC
1381 for (i = 1; i <= toc_count; i++) {
1382 if ((i > 1) && (toc_entry_level[i] == 1)) {
1383 print "" > toc_file;
1386 $0 = " " toc_entry_name[i] " ";
1387 if (length($0) % 2) { $0 = $0 " "; }
1388 while (length($0) < 76 - 4) {
1389 $0 = $0 " .";
1392 target_line = toc_entry_line[i] + toc_nb_lines;
1394 $0 = substr($0, 1, (76 - 5) - length(target_line)) " " target_line;
1395 print > toc_file;
1398 # Generate the Cross-Reference line number update script
1399 print "# Generated Cross-Reference script for SED" > xref_file;
1400 for (i = 1; i <= xref_idcount; i++) {
1401 if (anchor_address[xref_table[i]] != "") {
1402 target_line = anchor_address[xref_table[i]] + toc_nb_lines;
1403 } else if (node_address[xref_table[i]] != "") {
1404 target_line = node_address[xref_table[i]] + toc_nb_lines;
1405 } else {
1406 report_error("cross reference to undefined node/anchor \"" xref_table[i] "\" found at line " xref_defline[i]);
1408 printf "s/@x%02X@/%5d/g\n", i, target_line > xref_file;
1411 ' "$input_file" > "$temp_file" || exit $?
1413 # Run awk for 2nd pass, if it fails also stop now without deleting temp files
1414 awk '
1415 /@table_of_content@/ {
1416 while (getline < "'"$toc_file"'") {
1417 print;
1419 next;
1421 { print }
1422 ' "$temp_file" | sed -f "$xref_file" > "$output_file" || exit $?
1424 # If all worked, remove the temp files
1425 rm -f "$temp_file"
1426 rm -f "$toc_file"
1427 rm -f "$xref_file"