/ scripts / reflow.py
reflow.py
  1  #!/usr/bin/python3
  2  #
  3  # Copyright (c) 2016-2019 The Khronos Group Inc.
  4  #
  5  # Licensed under the Apache License, Version 2.0 (the "License");
  6  # you may not use this file except in compliance with the License.
  7  # You may obtain a copy of the License at
  8  #
  9  #     http://www.apache.org/licenses/LICENSE-2.0
 10  #
 11  # Unless required by applicable law or agreed to in writing, software
 12  # distributed under the License is distributed on an "AS IS" BASIS,
 13  # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 14  # See the License for the specific language governing permissions and
 15  # limitations under the License.
 16  
 17  # Used for automatic reflow of Vulkan spec to satisfy the agreed layout to
 18  # minimize git churn. Most of the logic has to do with detecting asciidoc
 19  # markup or block types that *shouldn't* be reflowed (tables, code) and
 20  # ignoring them. It's very likely there are many asciidoc constructs not yet
 21  # accounted for in the script, our usage of asciidoc markup is intentionally
 22  # somewhat limited.
 23  #
 24  # Also used to insert identifying tags on explicit Valid Usage statements.
 25  
 26  # Usage: reflow.py [-noflow] [-tagvu] [-nextvu #] [-overwrite] [-out dir] [-suffix str] files
 27  #   -noflow acts as a passthrough, instead of reflowing text. Other
 28  #       processing may occur.
 29  #   -tagvu generates explicit VUID tag for Valid Usage statements which
 30  #       don't already have them.
 31  #   -nextvu # starts VUID tag generation at the specified # instead of
 32  #       the value wired into the reflow.py script.
 33  #   -overwrite updates in place (can be risky, make sure there are backups)
 34  #   -out specifies directory to create output file in, default 'out'
 35  #   -suffix specifies suffix to add to output files, default ''
 36  #   files are asciidoc source files from the Vulkan spec to reflow.
 37  
 38  # For error and file-loading interfaces only
 39  import argparse
 40  import os
 41  import re
 42  import sys
 43  from reflib import loadFile, logDiag, logWarn, setLogFile
 44  from reflow_count import startVUID
 45  
 46  # Vulkan-specific - will consolidate into scripts/ like OpenXR soon
 47  sys.path.insert(0, 'xml')
 48  
 49  import vkapi as api
 50  from vkconventions import VulkanConventions as APIConventions
 51  conventions = APIConventions()
 52  
 53  # Markup that always ends a paragraph
 54  #   empty line or whitespace
 55  #   [block options]
 56  #   [[anchor]]
 57  #   //                  comment
 58  #   <<<<                page break
 59  #   :attribute-setting
 60  #   macro-directive::terms
 61  #   +                   standalone list item continuation
 62  #   label::             labelled list - label must be standalone
 63  endPara = re.compile(r'^( *|\[.*\]|//.*|<<<<|:.*|[a-z]+::.*|\+|.*::)$')
 64  
 65  # Special case of markup ending a paragraph, used to track the current
 66  # command/structure. This allows for either OpenXR or Vulkan API path
 67  # conventions. Nominally it should use the file suffix defined by the API
 68  # conventions (conventions.file_suffix), except that XR uses '.txt' for
 69  # generated API include files, not '.adoc' like its other includes.
 70  includePat = re.compile(
 71          r'include::(?P<directory_traverse>((../){1,4}|\{INCS-VAR\}/|\{generated\}/)(generated/)?)(?P<generated_type>[\w]+)/(?P<category>\w+)/(?P<entity_name>[^./]+).txt[\[][\]]')
 72  
 73  # Find the first pname: pattern in a Valid Usage statement
 74  pnamePat = re.compile(r'pname:(?P<param>\w+)')
 75  
 76  # Markup that's OK in a contiguous paragraph but otherwise passed through
 77  #   .anything
 78  #   === Section Titles
 79  endParaContinue = re.compile(r'^(\..*|=+ .*)$')
 80  
 81  # Markup for block delimiters whose contents *should* be reformatted
 82  #   --   (exactly two)  (open block)
 83  #   **** (4 or more)    (sidebar block - why do we have these?!)
 84  #   ==== (4 or more)    (example block)
 85  #   ____ (4 or more)    (quote block)
 86  blockReflow = re.compile(r'^(--|[*=_]{4,})$')
 87  # Fake block delimiters for "common" VU statements
 88  blockCommonReflow = '// Common Valid Usage\n'
 89  import pdb
 90  
 91  # Markup for block delimiters whose contents should *not* be reformatted
 92  #   |=== (3 or more)  (table)
 93  #   ++++ (4 or more)  (passthrough block)
 94  #   .... (4 or more)  (literal block)
 95  #   //// (4 or more)  (comment block)
 96  #   ---- (4 or more)  (listing block)
 97  #   **** (4 or more)  (sidebar block)
 98  blockPassthrough = re.compile(r'^(\|={3,}|[-+./]{4,})$')
 99  
100  # Markup for introducing bullet points (hanging paragraphs)
101  #   * bullet
102  #     ** bullet
103  #     -- bullet
104  #   . bullet
105  #   :: bullet
106  beginBullet = re.compile(r'^ *([*-.]+|::) ')
107  
108  # Text that (may) not end sentences
109  
110  # A single letter followed by a period, typically a middle initial.
111  endInitial = re.compile(r'^[A-Z]\.$')
112  # An abbreviation, which doesn't (usually) end a line.
113  endAbbrev = re.compile(r'(e\.g|i\.e|c\.f)\.$', re.IGNORECASE)
114  
115  # State machine for reflowing.
116  #
117  # blockStack - The last element is a line with the asciidoc block delimiter
118  #   that's currently in effect, such as
119  #     '--', '----', '****', '======', or '+++++++++'.
120  #   This affects whether or not the block contents should be formatted.
121  # reflowStack - The last element is True or False if the current blockStack
122  #   contents should be reflowed.
123  # vuStack - the last element is True or False if the current blockStack
124  #   contents are an explicit Valid Usage block.
125  # margin - margin to reflow text to.
126  # para - list of lines in the paragraph being accumulated. When this is
127  #   non-empty, there is a current paragraph.
128  # lastTitle - true if the previous line was a document title line (e.g.
129  #   :leveloffset: 0 - no attempt to track changes to this is made).
130  # leadIndent - indent level (in spaces) of the first line of a paragraph.
131  # hangIndent - indent level of the remaining lines of a paragraph.
132  # file - file pointer to write to.
133  # filename - base name of file being read from.
134  # lineNumber - line number being read from the input file.
135  # breakPeriod - True if justification should break to a new line after
136  #   the end of a sentence.
137  # breakInitial - True if justification should break to a new line after
138  #   something that appears to be an initial in someone's name. **TBD**
139  # reflow - True if text should be reflowed, False to pass through unchanged.
140  # vuPrefix - Prefix of generated Valid Usage tags
141  # vuFormat - Format string for generating Valid Usage tags. First argument
142  #   is vuPrefix, second is command/struct name, third is parameter name,
143  #   fourth is the tag number.
144  # nextvu - Integer to start tagging un-numbered Valid Usage statements with,
145  #   or None if no tagging should be done.
146  # apiName - String name of a Vulkan structure or command for VUID tag
147  #   generation, or None if one hasn't been included in this file yet.
148  class ReflowState:
149      """Represents the state of the reflow operation"""
150      def __init__(self,
151                   filename,
152                   margin = 76,
153                   file = sys.stdout,
154                   breakPeriod = True,
155                   reflow = True,
156                   nextvu = None):
157          self.blockStack = [ None ]
158          self.reflowStack = [ True ]
159          self.vuStack = [ False ]
160          self.margin = margin
161          self.para = []
162          self.lastTitle = False
163          self.leadIndent = 0
164          self.hangIndent = 0
165          self.file = file
166          self.filename = filename
167          self.lineNumber = 0
168          self.breakPeriod = breakPeriod
169          self.breakInitial = True
170          self.reflow = reflow
171          self.vuPrefix = 'VUID'
172          self.vuFormat = '{0}-{1}-{2}-{3:0>5d}'
173          self.nextvu = nextvu
174          self.apiName = ''
175  
176      def incrLineNumber(self):
177          self.lineNumber = self.lineNumber + 1
178  
179      # Print an array of lines with newlines already present
180      def printLines(self, lines):
181          logDiag(':: printLines:', len(lines), 'lines: ', lines[0], end='')
182          for line in lines:
183              print(line, file=self.file, end='')
184  
185      # Returns True if word ends with a sentence-period, False otherwise.
186      # Allows for contraction cases which won't end a line:
187      #  - A single letter (if breakInitial is True)
188      #  - Abbreviations: 'c.f.', 'e.g.', 'i.e.' (or mixed-case versions)
189      def endSentence(self, word):
190          if (word[-1:] != '.' or
191              endAbbrev.search(word) or
192                  (self.breakInitial and endInitial.match(word))):
193              return False
194  
195          return True
196  
197      # Returns True if word is a Valid Usage ID Tag anchor.
198      def vuidAnchor(self, word):
199          return (word[0:7] == '[[VUID-')
200  
201      # Reflow the current paragraph, respecting the paragraph lead and
202      # hanging indentation levels. The algorithm also respects trailing '+'
203      # signs that indicate embedded newlines, and will not reflow a very long
204      # word immediately after a bullet point.
205      # Just return the paragraph unchanged if the -noflow argument was
206      # given.
207      def reflowPara(self):
208          if not self.reflow:
209              return self.para
210  
211          logDiag('reflowPara lead indent = ', self.leadIndent,
212                  'hangIndent =', self.hangIndent,
213                  'para:', self.para[0], end='')
214  
215          # Total words processed (we care about the *first* word vs. others)
216          wordCount = 0
217  
218          # Tracks the *previous* word processed. It must not be empty.
219          prevWord = ' '
220  
221          #import pdb; pdb.set_trace()
222  
223          for line in self.para:
224              line = line.rstrip()
225              words = line.split()
226  
227              # logDiag('reflowPara: input line =', line)
228              numWords = len(words) - 1
229  
230              for i in range(0, numWords + 1):
231                  word = words[i]
232                  wordLen = len(word)
233                  wordCount += 1
234  
235                  endEscape = False
236                  if i == numWords and word == '+':
237                      # Trailing ' +' must stay on the same line
238                      endEscape = word
239                      # logDiag('reflowPara last word of line =', word, 'prevWord =', prevWord, 'endEscape =', endEscape)
240                  else:
241                      pass
242                      # logDiag('reflowPara wordCount =', wordCount, 'word =', word, 'prevWord =', prevWord)
243  
244                  if wordCount == 1:
245                      # The first word of the paragraph is treated specially.
246                      # The loop logic becomes trickier if all this code is
247                      # done prior to looping over lines and words, so all the
248                      # setup logic is done here.
249  
250                      outPara = []
251                      outLine = ''.ljust(self.leadIndent) + word
252                      outLineLen = self.leadIndent + wordLen
253  
254                      # If the paragraph begins with a bullet point, generate
255                      # a hanging indent level if there isn't one already.
256                      if beginBullet.match(self.para[0]):
257                          bulletPoint = True
258                          if len(self.para) > 1:
259                              logDiag('reflowPara first line matches bullet point',
260                                      'but indent already hanging @ input line',
261                                      self.lineNumber)
262                          else:
263                              logDiag('reflowPara first line matches bullet point -'
264                                      'single line, assuming hangIndent @ input line',
265                                      self.lineNumber)
266                              self.hangIndent = outLineLen + 1
267                      else:
268                          bulletPoint = False
269                  else:
270                      # Possible actions to take with this word
271                      #
272                      # addWord - add word to current line
273                      # closeLine - append line and start a new (null) one
274                      # startLine - add word to a new line
275  
276                      # Default behavior if all the tests below fail is to add
277                      # this word to the current line, and keep accumulating
278                      # that line.
279                      (addWord, closeLine, startLine) = (True, False, False)
280  
281                      # How long would this line be if the word were added?
282                      newLen = outLineLen + 1 + wordLen
283  
284                      # Are we on the first word following a bullet point?
285                      firstBullet = (wordCount == 2 and bulletPoint)
286  
287                      if endEscape:
288                          # If the new word ends the input line with ' +',
289                          # add it to the current line.
290  
291                          (addWord, closeLine, startLine) = (True, True, False)
292                      elif self.vuidAnchor(word):
293                          # If the new word is a Valid Usage anchor, break the
294                          # line afterwards. Note that this should only happen
295                          # immediately after a bullet point, but we don't
296                          # currently check for this.
297                          (addWord, closeLine, startLine) = (True, True, False)
298                      elif newLen > self.margin:
299                          if firstBullet:
300                              # If the word follows a bullet point, add it to
301                              # the current line no matter its length.
302  
303                              (addWord, closeLine, startLine) = (True, True, False)
304                          else:
305                              # The word overflows, so add it to a new line.
306  
307                              (addWord, closeLine, startLine) = (False, True, True)
308                      elif (self.breakPeriod and
309                            (wordCount > 2 or not firstBullet) and
310                            self.endSentence(prevWord)):
311                          # If the previous word ends a sentence and
312                          # breakPeriod is set, start a new line.
313                          # The complicated logic allows for leading bullet
314                          # points which are periods (implicitly numbered lists).
315                          # @@@ But not yet for explicitly numbered lists.
316  
317                          (addWord, closeLine, startLine) = (False, True, True)
318  
319                      # Add a word to the current line
320                      if addWord:
321                          if outLine:
322                              outLine += ' ' + word
323                              outLineLen = newLen
324                          else:
325                              # Fall through to startLine case if there's no
326                              # current line yet.
327                              startLine = True
328  
329                      # Add current line to the output paragraph. Force
330                      # starting a new line, although we don't yet know if it
331                      # will ever have contents.
332                      if closeLine:
333                          if outLine:
334                              outPara.append(outLine + '\n')
335                              outLine = None
336  
337                      # Start a new line and add a word to it
338                      if startLine:
339                          outLine = ''.ljust(self.hangIndent) + word
340                          outLineLen = self.hangIndent + wordLen
341  
342                  # Track the previous word, for use in breaking at end of
343                  # a sentence
344                  prevWord = word
345  
346          # Add this line to the output paragraph.
347          if outLine:
348              outPara.append(outLine + '\n')
349  
350          return outPara
351  
352      # Emit a paragraph, possibly reflowing it depending on the block
353      # context. Reset the paragraph accumulator.
354      def emitPara(self):
355          if self.para != []:
356              if self.vuStack[-1] and self.nextvu is not None:
357                  # If:
358                  #   - this paragraph is in a Valid Usage block,
359                  #   - VUID tags are being assigned,
360                  # Try to assign VUIDs
361  
362                  if nestedVuPat.search(self.para[0]):
363                      # Check for nested bullet points. These should not be
364                      # assigned VUIDs, nor present at all, because they break
365                      # the VU extractor.
366                      logWarn(self.filename + ': Invalid nested bullet point in VU block:', self.para[0])
367                  elif self.vuPrefix not in self.para[0]:
368                      # If:
369                      #   - a tag is not already present, and
370                      #   - the paragraph is a properly marked-up list item
371                      # Then add a VUID tag starting with the next free ID.
372  
373                      # Split the first line after the bullet point
374                      matches = vuPat.search(self.para[0])
375                      if matches is not None:
376                          logDiag('findRefs: Matched vuPat on line:', self.para[0], end='')
377                          head = matches.group('head')
378                          tail = matches.group('tail')
379  
380                          # Use the first pname: statement in the paragraph as
381                          # the parameter name in the VUID tag. This won't always
382                          # be correct, but should be highly reliable.
383                          for vuLine in self.para:
384                              matches = pnamePat.search(vuLine)
385                              if matches is not None:
386                                  break
387  
388                          if matches is not None:
389                              paramName = matches.group('param')
390                          else:
391                              paramName = 'None'
392                              logWarn(self.filename,
393                                      'No param name found for VUID tag on line:',
394                                      self.para[0])
395  
396                          newline = (head + ' [[' +
397                                     self.vuFormat.format(self.vuPrefix,
398                                                          self.apiName,
399                                                          paramName,
400                                                          self.nextvu) + ']] ' + tail)
401  
402                          logDiag('Assigning', self.vuPrefix, self.apiName, self.nextvu,
403                                  ' on line:', self.para[0], '->', newline, 'END')
404  
405                          self.para[0] = newline
406                          self.nextvu = self.nextvu + 1
407                  # else:
408                  #     There are only a few cases of this, and they're all
409                  #     legitimate. Leave detecting this case to another tool
410                  #     or hand inspection.
411                  #     logWarn(self.filename + ': Unexpected non-bullet item in VU block (harmless if following an ifdef):',
412                  #             self.para[0])
413  
414              if self.reflowStack[-1]:
415                  self.printLines(self.reflowPara())
416              else:
417                  self.printLines(self.para)
418  
419          # Reset the paragraph, including its indentation level
420          self.para = []
421          self.leadIndent = 0
422          self.hangIndent = 0
423  
424      # 'line' ends a paragraph and should itself be emitted.
425      # line may be None to indicate EOF or other exception.
426      def endPara(self, line):
427          logDiag('endPara line', self.lineNumber, ': emitting paragraph')
428  
429          # Emit current paragraph, this line, and reset tracker
430          self.emitPara()
431  
432          if line:
433              self.printLines( [ line ] )
434  
435      # 'line' ends a paragraph (unless there's already a paragraph being
436      # accumulated, e.g. len(para) > 0 - currently not implemented)
437      def endParaContinue(self, line):
438          self.endPara(line)
439  
440      # 'line' begins or ends a block. If beginning a block, tag whether or
441      # not to reflow the contents.
442      # vuBlock is True if the previous line indicates this is a Valid Usage
443      # block.
444      def endBlock(self, line, reflow = False, vuBlock = False):
445          self.endPara(line)
446  
447          if self.blockStack[-1] == line:
448              logDiag('endBlock line', self.lineNumber,
449                      ': popping block end depth:', len(self.blockStack),
450                      ':', line, end='')
451              self.blockStack.pop()
452              self.reflowStack.pop()
453              self.vuStack.pop()
454          else:
455              # Start a block
456              self.blockStack.append(line)
457              self.reflowStack.append(reflow)
458              self.vuStack.append(vuBlock)
459  
460              logDiag('endBlock reflow =', reflow, ' line', self.lineNumber,
461                      ': pushing block start depth', len(self.blockStack),
462                      ':', line, end='')
463  
464      # 'line' begins or ends a block. The paragraphs in the block *should* be
465      # reformatted (e.g. a NOTE).
466      def endParaBlockReflow(self, line, vuBlock):
467          self.endBlock(line, reflow = True, vuBlock = vuBlock)
468  
469      # 'line' begins or ends a block. The paragraphs in the block should
470      # *not* be reformatted (e.g. a NOTE).
471      def endParaBlockPassthrough(self, line):
472          self.endBlock(line, reflow = False)
473  
474      # 'line' starts or continues a paragraph.
475      # Paragraphs may have "hanging indent", e.g.
476      #   * Bullet point...
477      #     ... continued
478      # In this case, when the higher indentation level ends, so does the
479      # paragraph.
480      def addLine(self, line):
481          logDiag('addLine line', self.lineNumber, ':', line, end='')
482  
483          # See https://stackoverflow.com/questions/13648813/what-is-the-pythonic-way-to-count-the-leading-spaces-in-a-string
484          indent = len(line) - len(line.lstrip())
485  
486          # A hanging paragraph ends due to a less-indented line.
487          if self.para != [] and indent < self.hangIndent:
488              logDiag('addLine: line reduces indentation, emit paragraph')
489              self.emitPara()
490  
491          # A bullet point (or something that looks like one) always ends the
492          # current paragraph.
493          if beginBullet.match(line):
494              logDiag('addLine: line matches beginBullet, emit paragraph')
495              self.emitPara()
496  
497          if self.para == []:
498              # Begin a new paragraph
499              self.para = [ line ]
500              self.leadIndent = indent
501              self.hangIndent = indent
502          else:
503              # Add a line to a paragraph. Increase the hanging indentation
504              # level - once.
505              if self.hangIndent == self.leadIndent:
506                  self.hangIndent = indent
507              self.para.append(line)
508  
509  def reflowFile(filename, args):
510      logDiag('reflow: filename', filename)
511  
512      lines = loadFile(filename)
513      if lines is None:
514          return
515  
516      # Output file handle and reflow object for this file. There are no race
517      # conditions on overwriting the input, but it's not recommended unless
518      # you have backing store such as git.
519  
520      if args.overwrite:
521          outFilename = filename
522      else:
523          outFilename = args.outDir + '/' + os.path.basename(filename) + args.suffix
524  
525      try:
526          fp = open(outFilename, 'w', encoding='utf8')
527      except:
528          logWarn('Cannot open output file', filename, ':', sys.exc_info()[0])
529          return
530  
531      state = ReflowState(filename,
532                          file = fp,
533                          reflow = not args.noflow,
534                          nextvu = args.nextvu)
535  
536      for line in lines:
537          state.incrLineNumber()
538  
539          # Is this a title line (leading '= ' followed by text)?
540          thisTitle = False
541  
542          # The logic here is broken. If we're in a non-reflowable block and
543          # this line *doesn't* end the block, it should always be
544          # accumulated.
545  
546          # Test for a blockCommonReflow delimiter comment first, to avoid
547          # treating it solely as a end-Paragraph marker comment.
548          if line == blockCommonReflow:
549              # Starting or ending a pseudo-block for "common" VU statements.
550  
551              # Common VU statements use an Asciidoc variable as the apiName,
552              # instead of inferring it from the most recent API include.
553              state.apiName = '{refpage}'
554              state.endParaBlockReflow(line, vuBlock = True)
555  
556          elif blockReflow.match(line):
557              # Starting or ending a block whose contents may be reflowed.
558              # Blocks cannot be nested.
559  
560              # Is this is an explicit Valid Usage block?
561              vuBlock = (state.lineNumber > 1 and
562                         lines[state.lineNumber-2] == '.Valid Usage\n')
563  
564              state.endParaBlockReflow(line, vuBlock)
565  
566          elif endPara.match(line):
567              # Ending a paragraph. Emit the current paragraph, if any, and
568              # prepare to begin a new paragraph.
569  
570              state.endPara(line)
571  
572              # If this is an include:: line starting the definition of a
573              # structure or command, track that for use in VUID generation.
574  
575              matches = includePat.search(line)
576              if matches is not None:
577                  include_type = matches.group('category')
578                  if include_type in ('protos', 'structs'):
579                      state.apiName = matches.group('entity_name')
580  
581          elif endParaContinue.match(line):
582              # For now, always just end the paragraph.
583              # Could check see if len(para) > 0 to accumulate.
584  
585              state.endParaContinue(line)
586  
587              # If it's a title line, track that
588              if line[0:2] == '= ':
589                  thisTitle = True
590  
591          elif blockPassthrough.match(line):
592              # Starting or ending a block whose contents must not be reflowed.
593              # These are tables, etc. Blocks cannot be nested.
594  
595              state.endParaBlockPassthrough(line)
596          elif state.lastTitle:
597              # The previous line was a document title line. This line
598              # is the author / credits line and must not be reflowed.
599  
600              state.endPara(line)
601          else:
602              # Just accumulate a line to the current paragraph. Watch out for
603              # hanging indents / bullet-points and track that indent level.
604  
605              state.addLine(line)
606  
607          state.lastTitle = thisTitle
608  
609      # Cleanup at end of file
610      state.endPara(None)
611  
612      # Sanity check on block nesting
613      if len(state.blockStack) > 1:
614          logWarn('file', filename,
615                  'mismatched asciidoc block delimiters at EOF:',
616                  state.blockStack[-1])
617  
618      fp.close()
619  
620      # Update the 'nextvu' value
621      if args.nextvu != state.nextvu:
622          logWarn('Updated nextvu to', state.nextvu, 'after file', filename)
623          args.nextvu = state.nextvu
624  
625  def reflowAllAdocFiles(folder_to_reflow, args):
626      for root, subdirs, files in os.walk(folder_to_reflow):
627          for file in files:
628              if file.endswith(conventions.file_suffix):
629                  file_path = os.path.join(root, file)
630                  reflowFile(file_path, args)
631          for subdir in subdirs:
632              sub_folder = os.path.join(root, subdir)
633              print('Sub-folder = %s' % sub_folder)
634              if subdir.lower() not in conventions.spec_no_reflow_dirs:
635                  print('   Parsing = %s' % sub_folder)
636                  reflowAllAdocFiles(sub_folder, args)
637              else:
638                  print('   Skipping = %s' % sub_folder)
639  
640  # Patterns used to recognize interesting lines in an asciidoc source file.
641  # These patterns are only compiled once.
642  
643  # Explicit Valid Usage list item with one or more leading asterisks
644  # The re.DOTALL is needed to prevent vuPat.search() from stripping
645  # the trailing newline.
646  vuPat = re.compile(r'^(?P<head>  [*]+)( *)(?P<tail>.*)', re.DOTALL)
647  
648  # Pattern matching leading nested bullet points
649  global nestedVuPat
650  nestedVuPat = re.compile(r'^  \*\*')
651  
652  if __name__ == '__main__':
653      parser = argparse.ArgumentParser()
654  
655      parser.add_argument('-diag', action='store', dest='diagFile',
656                          help='Set the diagnostic file')
657      parser.add_argument('-warn', action='store', dest='warnFile',
658                          help='Set the warning file')
659      parser.add_argument('-log', action='store', dest='logFile',
660                          help='Set the log file for both diagnostics and warnings')
661      parser.add_argument('-overwrite', action='store_true',
662                          help='Overwrite input filenames instead of writing different output filenames')
663      parser.add_argument('-out', action='store', dest='outDir',
664                          default='out',
665                          help='Set the output directory in which updated files are generated (default: out)')
666      parser.add_argument('-tagvu', action='store_true',
667                          help='Tag un-tagged Valid Usage statements starting at the value wired into reflow.py')
668      parser.add_argument('-nextvu', action='store', dest='nextvu', type=int,
669                          default=None,
670                          help='Tag un-tagged Valid Usage statements starting at the specified base VUID instead of the value wired into reflow.py')
671      parser.add_argument('-noflow', action='store_true', dest='noflow',
672                          help='Do not reflow text. Other actions may apply.')
673      parser.add_argument('-suffix', action='store', dest='suffix',
674                          default='',
675                          help='Set the suffix added to updated file names (default: none)')
676      parser.add_argument('files', metavar='filename', nargs='*',
677                          help='a filename to reflow text in')
678      parser.add_argument('--version', action='version', version='%(prog)s 1.0')
679  
680      args = parser.parse_args()
681  
682      setLogFile(True,  True, args.logFile)
683      setLogFile(True, False, args.diagFile)
684      setLogFile(False, True, args.warnFile)
685  
686      if args.overwrite:
687          logWarn('reflow.py: will overwrite all input files')
688  
689      if args.tagvu and args.nextvu is None:
690          args.nextvu = startVUID
691  
692      if args.nextvu is not None:
693          logWarn('Tagging untagged Valid Usage statements starting at', args.nextvu)
694  
695      # If no files are specified, reflow the entire specification chapters folder
696      if not args.files:
697          folder_to_reflow = os.getcwd()
698          folder_to_reflow += '/' + conventions.spec_reflow_path
699          reflowAllAdocFiles(folder_to_reflow, args)
700      else:
701          for file in args.files:
702              reflowFile(file, args)
703  
704      if args.nextvu is not None and args.nextvu != startVUID:
705          try:
706              reflow_count_file_path = os.path.dirname(os.path.realpath(__file__))
707              reflow_count_file_path += '/reflow_count.py'
708              reflow_count_file = open(reflow_count_file_path, 'w', encoding='utf8')
709              print('# The value to start tagging VU statements at, unless overridden by -nextvu\n', file=reflow_count_file, end='')
710              count_string = 'startVUID = %d\n' % args.nextvu
711              print(count_string, file=reflow_count_file, end='')
712              reflow_count_file.close()
713          except:
714              logWarn('Cannot open output count file reflow_count.py', ':', sys.exc_info()[0])