2 # -*- coding: utf-8 -*-
3 # extract_texi_filenames.py
5 # USAGE: extract_texi_filenames.py [-o OUTDIR] FILES
7 # -o OUTDIR specifies that output files should rather be written in OUTDIR
10 # This script parses the .texi file given and creates a file with the
11 # nodename <=> filename/anchor map.
12 # The idea behind: Unnumbered subsections go into the same file as the
13 # previous numbered section, @translationof gives the original node name,
14 # which is then used for the filename/anchor.
16 # If this script is run on a file texifile.texi, it produces a file
17 # texifile[.LANG].xref-map with tab-separated entries of the form
18 # NODE\tFILENAME\tANCHOR
19 # LANG is the document language in case it's not 'en'
20 # Note: The filename does not have any extension appended!
21 # This file should then be used by our texi2html init script to determine
22 # the correct file name and anchor for external refs
24 # For translated documentation: cross-references to nodes that exist
25 # only in documentation in English are allowed, that's why the already
26 # generated map file of docs in English is loaded with
27 # --master-map-file option, then the node names that are defined in
28 # the map for the manual in English but not in the translated manual
29 # are added to the map for the translated manual.
37 options_list, files = getopt.getopt (sys.argv[1:],'o:s:hI:m:k:q',
41 'known-missing-files=',
44 help_text = r"""Usage: %(program_name)s [OPTIONS]... TEXIFILE...
45 Extract files names for texinfo (sub)sections from the texinfo files.
48 -h, --help print this help
49 -I, --include=DIRECTORY append DIRECTORY to include search path
50 -m, --master-map-file=FILE use FILE as master map file
51 -o, --output=DIRECTORY write .xref-map files to DIRECTORY
52 -s, --split=MODE split manual according to MODE. Possible values
53 are section and custom (default)
54 -k, --known-missing-files a filename which has a list of files known
55 to be missing for this make
56 -q, --quiet suppress most messages
60 sys.stdout.write ( text)
67 known_missing_files = []
68 known_missing_files_file = ''
69 docs_without_directories = ['changes', 'music-glossary']
70 suppress_output = False
72 for opt in options_list:
75 if o == '-h' or o == '--help':
76 help (help_text % vars ())
77 if o == '-I' or o == '--include':
79 include_path.append (a)
81 path_list = a.split('/')
82 file_name = path_list[len(path_list)-1]
83 if not (file_name in docs_without_directories):
84 print a, 'is not a directory.'
85 print 'Please consider adding it to the list of '
86 print 'known missing files in extract_texi_filename.py.'
87 elif o == '-o' or o == '--output':
89 elif o == '-s' or o == '--split':
91 elif o == '-m' or o == '--master-map-file':
92 if os.path.isfile (a):
94 elif o == '--known-missing-files':
95 if os.path.isfile (a):
96 known_missing_files_file = a
98 print 'Missing files list file not found: ', a
99 elif o == '-q' or o == '--quiet':
100 suppress_output = True
102 raise Exception ('unknown option: ' + o)
104 if known_missing_files_file:
105 missing_files = open (known_missing_files_file, 'r')
106 known_missing_files = missing_files.read().splitlines()
107 missing_files.close()
109 if not os.path.isdir (outdir):
110 if os.path.exists (outdir):
114 include_re = re.compile (r'@include ((?!../lily-).*?\.i?te(xi|ly))$', re.M)
115 whitespaces = re.compile (r'\s+')
116 section_translation_re = re.compile ('^@(node|(?:unnumbered|appendix)\
117 (?:(?:sub){0,2}sec)?|top|chapter|(?:sub){0,2}section|\
118 (?:major|chap|(?:sub){0,2})heading|lydoctitle|translationof) \
119 (.+)$', re.MULTILINE)
120 external_node_re = re.compile (r'\s+@c\s+external.*')
122 def expand_includes (m, filename):
123 include_name = m.group (1)
124 filepath = os.path.join (os.path.dirname (filename), include_name)
125 if os.path.exists (filepath):
126 return extract_sections (filepath)[1]
128 for directory in include_path:
129 filepath = os.path.join (directory, include_name)
130 if os.path.exists (filepath):
131 return extract_sections (filepath)[1]
132 if not (include_name in known_missing_files):
134 print 'No such file: ' + include_name
135 print 'Search path: ' + ':'.join (include_path)
138 lang_re = re.compile (r'^@documentlanguage (.+)', re.M)
140 def extract_sections (filename):
142 f = open (filename, 'r')
145 # Search document language
146 m = lang_re.search (page)
147 if m and m.group (1) != 'en':
148 lang_suffix = '.' + m.group (1)
151 # Replace all includes by their list of sections and extract all sections
152 page = include_re.sub (lambda m: expand_includes (m, filename), page)
153 sections = section_translation_re.findall (page)
155 result += "@" + sec[0] + " " + sec[1] + "\n"
156 return (lang_suffix, result)
158 # Convert a given node name to its proper file name (normalization as
159 # explained in the texinfo manual:
160 # http://www.gnu.org/software/texinfo/manual/texinfo/html_node/HTML-Xref-Node-Name-Expansion.html
161 def texinfo_file_name(title):
162 # exception: The top node is always mapped to index.html
165 # File name normalization by texinfo (described in the texinfo manual):
166 # 1/2: letters and numbers are left unchanged
167 # 3/4: multiple, leading and trailing whitespace is removed
168 title = title.strip ();
169 title = whitespaces.sub (' ', title)
170 # 5: all remaining spaces are converted to '-'
171 # 6: all other 7- or 8-bit chars are replaced by _xxxx (xxxx=ascii character code)
173 for index in range(len(title)):
175 if char == ' ': # space -> '-'
177 elif ( ('0' <= char and char <= '9' ) or
178 ('A' <= char and char <= 'Z' ) or
179 ('a' <= char and char <= 'z' ) ): # number or letter
184 result += "_%04x" % ccode
186 result += "__%06x" % ccode
187 # 7: if name begins with number, prepend 't_g' (so it starts with a letter)
188 if (result != '') and (ord(result[0]) in range (ord('0'), ord('9'))):
189 result = 't_g' + result
192 texinfo_re = re.compile (r'@.*?{(.*?)}')
193 def remove_texinfo (title):
194 title = title.replace ('--', '-')
195 return texinfo_re.sub (r'\1', title).strip ()
197 def create_texinfo_anchor (title):
198 return texinfo_file_name (remove_texinfo (title))
200 unnumbered_re = re.compile (r'unnumbered.+|lydoctitle')
201 file_name_section_level = {
210 'unnumberedsubsec':1,
213 'unnumberedsubsubsec':0,
214 'appendixsubsubsec':0
216 if split in file_name_section_level:
217 splitting_level = file_name_section_level[split]
220 def process_sections (filename, lang_suffix, page):
221 sections = section_translation_re.findall (page)
222 basename = os.path.splitext (os.path.basename (filename))[0]
223 p = os.path.join (outdir, basename) + lang_suffix + '.xref-map'
224 if not suppress_output:
229 this_filename = 'index'
231 this_unnumbered = False
235 # Write out the cached values to the file and start a new
237 if this_title and this_title != 'Top':
238 f.write (this_title + "\t" + this_filename + "\t" + this_anchor + "\n")
240 this_title = remove_texinfo (sec[1])
241 this_anchor = create_texinfo_anchor (sec[1])
242 # delete entry from master map file
243 if this_title in initial_map:
244 del initial_map[this_title]
245 elif sec[0] == "translationof":
246 (original_node, external_node) = external_node_re.subn ('', sec[1])
247 original_node = remove_texinfo (original_node)
248 # The following binds the translator to use the
249 # translated node name in cross-references in case
251 if external_node and original_node in initial_map:
252 del initial_map[original_node]
253 anchor = create_texinfo_anchor (sec[1])
254 # If @translationof is used, it gives the original
255 # node name, which we use for the anchor and the file
256 # name (if it is a numbered node)
258 if not this_unnumbered:
259 this_filename = anchor
260 elif original_node in initial_map:
261 this_filename = initial_map[original_node][2]
263 # Some pages might not use a node for every section, so
264 # treat this case here, too: If we already had a section
265 # and encounter another one before the next @node, we
266 # write out the old one and start with the new values
267 if had_section and split != 'node' and this_title:
268 f.write (this_title + "\t" + this_filename + "\t" + this_anchor + "\n")
269 this_title = remove_texinfo (sec[1])
270 this_anchor = create_texinfo_anchor (sec[1])
273 if split == 'custom':
274 # unnumbered nodes use the previously used file name,
275 # only numbered nodes get their own filename! However,
276 # top-level @unnumbered still get their own file.
277 this_unnumbered = unnumbered_re.match (sec[0])
278 if not this_unnumbered:
279 this_filename = this_anchor
280 elif split == 'node':
281 this_filename = this_anchor
283 if sec[0] in file_name_section_level and \
284 file_name_section_level[sec[0]] >= splitting_level:
285 this_filename = this_anchor
287 if this_title and this_title != 'Top':
288 f.write (this_title + "\t" + this_filename + "\t" + this_anchor + "\n")
290 for node in initial_map:
291 f.write ("\t".join (initial_map[node]) + "\n")
294 xref_map_line_re = re.compile (r'(.*?)\t(.*?)\t(.*?)$')
296 for line in open (master_map_file):
297 m = xref_map_line_re.match (line)
299 initial_map[m.group (1)] = (m.group (1), m.group (2), m.group (3))
301 for filename in files:
302 if not suppress_output:
303 print "extract_texi_filenames.py: Processing %s" % filename
304 (lang_suffix, sections) = extract_sections (filename)
305 process_sections (filename, lang_suffix, sections)