|  | #!/usr/bin/env dart | 
|  | // Copyright (c) 2014, the Dart project authors.  Please see the AUTHORS file | 
|  | // for details. All rights reserved. Use of this source code is governed by a | 
|  | // BSD-style license that can be found in the LICENSE file. | 
|  |  | 
|  | // ---------------------------------------------------------------------- | 
|  | // This is a very specialized tool which was created in order to support | 
|  | // adding hash values used as location markers in the LaTeX source of the | 
|  | // language specification.  It is intended to take its input file as the | 
|  | // first argument, an output file name as the second argument, and a | 
|  | // hash listing file name as the third argument. From docs/language a | 
|  | // typical usage would be as follows: | 
|  | // | 
|  | // dart ../../tools/addlatexhash.dart dartLangSpec.tex out.tex hash.txt | 
|  | // | 
|  | // This will produce a normalized variant out.tex of the language | 
|  | // specification with hash values filled in, and a listing hash.txt of | 
|  | // all the hash values along with the label of their textual context | 
|  | // (section, subsection, subsubsection, paragraph) .  For more details, | 
|  | // please check the language specification source itself. | 
|  | // | 
|  | // NB: This utility assumes UN*X style line endings, \n, in the LaTeX | 
|  | // source file received as input; it will not work with other styles. | 
|  |  | 
|  | // ignore_for_file: constant_identifier_names, only_throw_errors | 
|  |  | 
|  | import 'dart:convert'; | 
|  | import 'dart:io'; | 
|  |  | 
|  | import 'package:convert/convert.dart'; | 
|  | import 'package:crypto/crypto.dart'; | 
|  |  | 
|  | // ---------------------------------------------------------------------- | 
|  | // Normalization of the text: removal or normalization of parts that | 
|  | // do not affect the output from latex, such as white space. | 
|  |  | 
|  | final commentRE = RegExp(r'[^\\]%.*'); // NB: . does not match \n. | 
|  | final whitespaceAllRE = RegExp(r'^\s+$'); | 
|  | final whitespaceRE = RegExp(r'(?:(?=\s).){2,}'); // \s except end-of-line | 
|  |  | 
|  | /// Removes [match]ing part of [line], adjusting that part with the | 
|  | /// given [startOffset] and [endOffset], bounded to be valid indices | 
|  | /// into the string if needed, then inserts [glue] where text was | 
|  | /// removed.  If there is no match then [line] is returned. | 
|  | String cutMatch(String line, RegExpMatch? match, | 
|  | {int startOffset = 0, int endOffset = 0, String glue = ''}) { | 
|  | if (match == null) return line; | 
|  | var start = match.start + startOffset; | 
|  | var end = match.end + endOffset; | 
|  | var len = line.length; | 
|  | if (start < 0) start = 0; | 
|  | if (end > len) end = len; | 
|  | return line.substring(0, start) + glue + line.substring(end); | 
|  | } | 
|  |  | 
|  | String cutRegexp(String line, RegExp re, | 
|  | {int startOffset = 0, int endOffset = 0, String glue = ''}) { | 
|  | return cutMatch(line, re.firstMatch(line), | 
|  | startOffset: startOffset, endOffset: endOffset, glue: glue); | 
|  | } | 
|  |  | 
|  | bool isWsOnly(String line) => line.contains(whitespaceAllRE); | 
|  | bool isCommentOnly(String line) => line.startsWith('%'); | 
|  |  | 
|  | /// Returns the end-of-line character at the end of [line], if any, | 
|  | /// otherwise returns the empty string. | 
|  | String justEol(String line) => line.endsWith('\n') ? '\n' : ''; | 
|  |  | 
|  | /// Removes the contents of the comment at the end of [line], | 
|  | /// leaving the "%" in place.  If no comment is present, | 
|  | /// return [line]. | 
|  | /// | 
|  | /// NB: it is tempting to remove everything from the '%' and out, | 
|  | /// including the final newline, if any, but this does not work. | 
|  | /// The problem is that TeX will do exactly this, but then it will | 
|  | /// add back a character that depends on its state (S, M, or N), | 
|  | /// and it is tricky to maintain a similar state that matches the | 
|  | /// state of TeX faithfully.  Hence, we remove the content of | 
|  | /// comments but do not remove the comments themselves, we just | 
|  | /// leave the '%' at the end of the line and let TeX manage its | 
|  | /// states in a way that does not differ from the file from before | 
|  | /// stripComment. | 
|  | String stripComment(String line) { | 
|  | if (isCommentOnly(line)) return '%\n'; | 
|  | return cutRegexp(line, commentRE, startOffset: 2); | 
|  | } | 
|  |  | 
|  | /// Reduces a white-space-only [line] to its eol character, | 
|  | /// removes leading ws entirely, and reduces multiple | 
|  | /// white-space chars to one. | 
|  | String normalizeWhitespace(String line) { | 
|  | var trimLine = line.trimLeft(); | 
|  | if (trimLine.isEmpty) return justEol(line); | 
|  | return trimLine.replaceAll(whitespaceRE, ' '); | 
|  | } | 
|  |  | 
|  | /// Reduces sequences of >1 white-space-only lines in [lines] to 1, | 
|  | /// and sequences of >1 comment-only lines to 1.  Treats comment-only | 
|  | /// lines as white-space-only when they occur in white-space-only | 
|  | /// line blocks. | 
|  | List<String> multilineNormalize(List<String> lines) { | 
|  | var afterBlankLines = false; // Does [line] succeed >0 empty lines? | 
|  | var afterCommentLines = false; // Does [line] succeed >0 commentOnly lines? | 
|  | var newLines = <String>[]; | 
|  | for (var line in lines) { | 
|  | if (afterBlankLines && afterCommentLines) { | 
|  | // Previous line was both blank and a comment: not possible. | 
|  | throw 'Bug, please report to eernst@'; | 
|  | } else if (afterBlankLines && !afterCommentLines) { | 
|  | // At least one line before [line] is wsOnly. | 
|  | if (!isWsOnly(line)) { | 
|  | // Blank line block ended. | 
|  | afterCommentLines = isCommentOnly(line); | 
|  | // Special case: It seems to be safe to remove commentOnly lines | 
|  | // after wsOnly lines, so the TeX state must be predictably right; | 
|  | // next line will then be afterCommentLines and be dropped, so | 
|  | // we drop the entire comment block---which is very useful.  We can | 
|  | // also consider this comment line to be an empty line, such that | 
|  | // subsequent empty lines can be considered to be in a block of | 
|  | // empty lines.  Note that almost all variants of this breaks. | 
|  | if (afterCommentLines) { | 
|  | // _Current_ 'line' is a commentOnly here. | 
|  | afterBlankLines = true; | 
|  | afterCommentLines = false; | 
|  | // Omit addition of [line]. | 
|  | } else { | 
|  | // After blanks, but current 'line' is neither blank nor comment. | 
|  | afterBlankLines = false; | 
|  | newLines.add(line); | 
|  | } | 
|  | } else { | 
|  | // Blank line block continues, omit addition of [line]. | 
|  | } | 
|  | } else if (!afterBlankLines && afterCommentLines) { | 
|  | // At least one line before [line] is commentOnly. | 
|  | if (!isCommentOnly(line)) { | 
|  | // Comment block ended. | 
|  | afterBlankLines = isWsOnly(line); | 
|  | afterCommentLines = false; | 
|  | newLines.add(line); | 
|  | } else { | 
|  | // Comment block continues, do not add [line]. | 
|  | } | 
|  | } else { | 
|  | assert(!afterBlankLines && !afterCommentLines); | 
|  | // No wsOnly or commentOnly lines precede [line]. | 
|  | afterBlankLines = isWsOnly(line); | 
|  | afterCommentLines = isCommentOnly(line); | 
|  | if (!afterCommentLines) { | 
|  | newLines.add(line); | 
|  | } else { | 
|  | // skip commentOnly line after nonWs/nonComment text. | 
|  | } | 
|  | } | 
|  | } | 
|  | return newLines; | 
|  | } | 
|  |  | 
|  | /// Selects the elements in the normalization pipeline. | 
|  | String normalize(String line) => normalizeWhitespace(stripComment(line)); | 
|  |  | 
|  | /// Selects the elements in the significant-spacing block | 
|  | /// normalization pipeline. | 
|  | String sispNormalize(String line) => stripComment(line); | 
|  |  | 
|  | // Managing fragments with significant spacing. | 
|  |  | 
|  | final dartCodeBeginRE = RegExp(r'^\s*\\begin\s*\{dartCode\}'); | 
|  | final dartCodeEndRE = RegExp(r'^\s*\\end\s*\{dartCode\}'); | 
|  |  | 
|  | /// Recognizes beginning of dartCode block. | 
|  | bool sispIsDartBegin(String line) => line.contains(dartCodeBeginRE); | 
|  |  | 
|  | /// Recognizes end of dartCode block. | 
|  | bool sispIsDartEnd(String line) => line.contains(dartCodeEndRE); | 
|  |  | 
|  | // ---------------------------------------------------------------------- | 
|  | // Analyzing the input to point out "interesting" lines | 
|  |  | 
|  | /// Returns the event information for [lines] as determined by the | 
|  | /// given [analyzer].  The method [HashAnalyzer.analyze] indicates that a | 
|  | /// line is "uninteresting" by returning null (i.e., no events here). | 
|  | List<HashEvent> findEvents(List<String> lines, HashAnalyzer analyzer) { | 
|  | var events = <HashEvent>[]; | 
|  | for (var line in lines) { | 
|  | var event = analyzer.analyze(line); | 
|  | if (event != null) events.add(event); | 
|  | } | 
|  | return events; | 
|  | } | 
|  |  | 
|  | /// Returns RegExp text for recognizing a command occupying a line | 
|  | /// of its own, given the part of the RegExp that recognizes the | 
|  | /// command name, [cmdNameRE] | 
|  | RegExp lineCommandRE(String cmdNameRE) => | 
|  | RegExp(r'^\s*\\' + cmdNameRE + r'\s*\{.*\}%?\s*$'); | 
|  |  | 
|  | final hashLabelStartRE = RegExp(r'^\s*\\LMLabel\s*\{'); | 
|  | final hashLabelEndRE = RegExp(r'\}\s*$'); | 
|  |  | 
|  | final hashMarkRE = lineCommandRE('LMHash'); | 
|  | final hashLabelRE = lineCommandRE('LMLabel'); | 
|  | final sectioningRE = lineCommandRE('((|sub(|sub))section|paragraph)'); | 
|  | final sectionRE = lineCommandRE('section'); | 
|  | final subsectionRE = lineCommandRE('subsection'); | 
|  | final subsubsectionRE = lineCommandRE('subsubsection'); | 
|  | final paragraphRE = lineCommandRE('paragraph'); | 
|  |  | 
|  | /// Returns true iff [line] begins a block of lines that gets a hash value. | 
|  | bool isHashMarker(String line) => line.contains(hashMarkRE); | 
|  |  | 
|  | /// Returns true iff [line] defines a sectioning label. | 
|  | bool isHashLabel(String line) => line.contains(hashLabelRE); | 
|  |  | 
|  | /// Returns true iff [line] is a sectioning command resp. one of its | 
|  | /// more specific forms; note that it is assumed that sectioning commands | 
|  | /// do not contain a newline between the command name and the '{'. | 
|  | bool isSectioningCommand(String line) => line.contains(sectioningRE); | 
|  | bool isSectionCommand(String line) => line.contains(sectionRE); | 
|  | bool isSubsectionCommand(String line) => line.contains(subsectionRE); | 
|  | bool isSubsubsectionCommand(String line) => line.contains(subsubsectionRE); | 
|  | bool isParagraphCommand(String line) => line.contains(paragraphRE); | 
|  |  | 
|  | /// Returns true iff [line] does not end a block of lines that gets | 
|  | /// a hash value. | 
|  | bool isntHashBlockTerminator(String line) => !isSectioningCommand(line); | 
|  |  | 
|  | /// Returns the label text part from [line], based on the assumption | 
|  | /// that isHashLabel(line) returns true. | 
|  | String extractHashLabel(String line) { | 
|  | var startMatch = hashLabelStartRE.firstMatch(line); | 
|  | var endMatch = hashLabelEndRE.firstMatch(line); | 
|  | if (startMatch != null && endMatch != null) { | 
|  | return line.substring(startMatch.end, endMatch.start); | 
|  | } else { | 
|  | throw 'Assertion failure (so this file is both valid nnbd and not)'; | 
|  | } | 
|  | } | 
|  |  | 
|  | // Event classes: Keep track of relevant information about the LaTeX | 
|  | // source code lines, such as where \LMHash and \LMLabel commands are | 
|  | // used, and how they are embedded in the sectioning structure. | 
|  |  | 
|  | /// Abstract events, enabling us to [setEndLineNumber] on all events. | 
|  | abstract class HashEvent { | 
|  | /// For events that have an endLineNumber, set it; otherwise ignore. | 
|  | /// The endLineNumber specifies the end of the block of lines | 
|  | /// associated with a given event, for event types concerned with | 
|  | /// blocks of lines rather than single lines. | 
|  | void setEndLineNumber(int n) {} | 
|  |  | 
|  | /// Returns null except for \LMHash{} events, where it returns | 
|  | /// the startLineNumber.  This serves to specify a boundary because | 
|  | /// the preceding \LMHash{} block should stop before the line of | 
|  | /// this \LMHash{} command.  Note that hash blocks may stop earlier, | 
|  | /// because they cannot contain sectioning commands. | 
|  | int? getStartLineNumber() => null; | 
|  | } | 
|  |  | 
|  | class HashMarkerEvent extends HashEvent { | 
|  | // Line number of first line in block that gets hashed. | 
|  | int startLineNumber; | 
|  |  | 
|  | // Highest possible number of first line after block that gets | 
|  | // hashed (where the next \LMHash{} occurs).  Note that this value | 
|  | // is not known initially (because that line has not yet been | 
|  | // reached), so [endLineNumber] will be initialized in a separate | 
|  | // scan.  Also note that the block may end earlier, because a block | 
|  | // ends if it would otherwise include a sectioning command. | 
|  | int? endLineNumber; | 
|  |  | 
|  | HashMarkerEvent(this.startLineNumber); | 
|  |  | 
|  | @override | 
|  | void setEndLineNumber(int n) { | 
|  | endLineNumber = n; | 
|  | } | 
|  |  | 
|  | @override | 
|  | int getStartLineNumber() => startLineNumber; | 
|  | } | 
|  |  | 
|  | class HashLabelEvent extends HashEvent { | 
|  | String labelText; | 
|  | HashLabelEvent(this.labelText); | 
|  | } | 
|  |  | 
|  | class HashAnalyzer { | 
|  | // List of kinds of pending (= most recently seen) sectioning command. | 
|  | // When updating this list, also update sectioningPrefix below. | 
|  | static const PENDING_IS_NONE = 0; | 
|  | static const PENDING_IS_SECTION = 1; | 
|  | static const PENDING_IS_SUBSECTION = 2; | 
|  | static const PENDING_IS_SUBSUBSECTION = 3; | 
|  | static const PENDING_IS_PARAGRAPH = 1; | 
|  |  | 
|  | int lineNumber = 0; | 
|  | int pendingSectioning = PENDING_IS_NONE; | 
|  |  | 
|  | HashAnalyzer(); | 
|  |  | 
|  | void setPendingToSection() { | 
|  | pendingSectioning = PENDING_IS_SECTION; | 
|  | } | 
|  |  | 
|  | void setPendingToSubsection() { | 
|  | pendingSectioning = PENDING_IS_SUBSECTION; | 
|  | } | 
|  |  | 
|  | void setPendingToSubsubsection() { | 
|  | pendingSectioning = PENDING_IS_SUBSUBSECTION; | 
|  | } | 
|  |  | 
|  | void setPendingToParagraph() { | 
|  | pendingSectioning = PENDING_IS_PARAGRAPH; | 
|  | } | 
|  |  | 
|  | String sectioningPrefix() { | 
|  | switch (pendingSectioning) { | 
|  | case PENDING_IS_SECTION: | 
|  | return 'sec:'; | 
|  | case PENDING_IS_SUBSECTION: | 
|  | return 'subsec:'; | 
|  | case PENDING_IS_SUBSUBSECTION: | 
|  | return 'subsubsec:'; | 
|  | case PENDING_IS_NONE: | 
|  | throw '\\LMHash{..} should only be used after a sectioning command ' | 
|  | '(\\section, \\subsection, \\subsubsection, \\paragraph)'; | 
|  | default: | 
|  | // set of PENDING_IS_.. was extended, but updates here omitted | 
|  | throw 'Bug, please report to eernst@'; | 
|  | } | 
|  | } | 
|  |  | 
|  | HashEvent? analyze(String line) { | 
|  | var currentLineNumber = lineNumber++; | 
|  | if (isHashMarker(line)) { | 
|  | return HashMarkerEvent(currentLineNumber); | 
|  | } else if (isHashLabel(line)) { | 
|  | var labelText = sectioningPrefix() + extractHashLabel(line); | 
|  | return HashLabelEvent(labelText); | 
|  | } else { | 
|  | // No events to emit, but we may need to note state changes | 
|  | if (isSectionCommand(line)) { | 
|  | setPendingToSection(); | 
|  | } else if (isSubsectionCommand(line)) { | 
|  | setPendingToSubsection(); | 
|  | } else if (isSubsubsectionCommand(line)) { | 
|  | setPendingToSubsubsection(); | 
|  | } else if (isParagraphCommand(line)) { | 
|  | setPendingToParagraph(); | 
|  | } else { | 
|  | // No state changes. | 
|  | } | 
|  | return null; | 
|  | } | 
|  | } | 
|  | } | 
|  |  | 
|  | List<HashEvent> findHashEvents(List<String> lines) { | 
|  | // Create the list of events, omitting endLineNumbers. | 
|  | var events = findEvents(lines, HashAnalyzer()); | 
|  | // Set the endLineNumbers. | 
|  | var currentEndLineNumber = lines.length; | 
|  | for (var event in events.reversed) { | 
|  | event.setEndLineNumber(currentEndLineNumber); | 
|  | var nextEndLineNumber = event.getStartLineNumber(); | 
|  | if (nextEndLineNumber != null) currentEndLineNumber = nextEndLineNumber; | 
|  | } | 
|  | return events; | 
|  | } | 
|  |  | 
|  | // ---------------------------------------------------------------------- | 
|  | // Removal of non-normative elements of the text (rationale, commentary). | 
|  |  | 
|  | /// Returns [line] without the command [cmdName] (based on a match | 
|  | /// on "\\cmdName\s*{..}") starting at [startIndex]; note that it is | 
|  | /// assumed but not checked that [line] contains "\\cmdType\s*{..", | 
|  | /// and note that the end of the {..} block is found via brace matching | 
|  | /// (i.e., nested {..} blocks are handled), but it may break if '{' is | 
|  | /// made an active character etc.etc. | 
|  | String removeCommand(String line, String cmdName, int startIndex) { | 
|  | const BACKSLASH = 92; // char code for '\\'. | 
|  | const BRACE_BEGIN = 123; // char code for '{'. | 
|  | const BRACE_END = 125; // char code for '}'. | 
|  |  | 
|  | var blockStartIndex = startIndex + cmdName.length + 1; | 
|  | while (blockStartIndex < line.length && | 
|  | line.codeUnitAt(blockStartIndex) != BRACE_BEGIN) { | 
|  | blockStartIndex++; | 
|  | } | 
|  | blockStartIndex++; | 
|  | if (blockStartIndex > line.length) { | 
|  | throw 'Bug, please report to eernst@'; | 
|  | } | 
|  | // [blockStartIndex] has index just after '{'. | 
|  |  | 
|  | var afterEscape = false; // Is true iff [index] is just after '{'. | 
|  | var braceLevel = 1; // Have seen so many '{'s minus so many '}'s. | 
|  |  | 
|  | for (var index = blockStartIndex; index < line.length; index++) { | 
|  | switch (line.codeUnitAt(index)) { | 
|  | case BRACE_BEGIN: | 
|  | if (afterEscape) { | 
|  | afterEscape = false; | 
|  | } else { | 
|  | braceLevel++; | 
|  | } | 
|  | break; | 
|  | case BRACE_END: | 
|  | if (afterEscape) { | 
|  | afterEscape = false; | 
|  | } else { | 
|  | braceLevel--; | 
|  | } | 
|  | break; | 
|  | case BACKSLASH: | 
|  | afterEscape = true; | 
|  | break; | 
|  | default: | 
|  | afterEscape = false; | 
|  | } | 
|  | if (braceLevel == 0) { | 
|  | return line.substring(0, startIndex) + line.substring(index + 1); | 
|  | } | 
|  | } | 
|  | // Removal failed; we consider this to mean that the input is ill-formed. | 
|  | throw 'Unmatched braces'; | 
|  | } | 
|  |  | 
|  | final commentaryRE = RegExp(r'\\commentary\s*\{'); | 
|  | final rationaleRE = RegExp(r'\\rationale\s*\{'); | 
|  |  | 
|  | /// Removes {}-balanced '\commentary{..}' commands from [line]. | 
|  | String removeCommentary(String line) { | 
|  | var match = commentaryRE.firstMatch(line); | 
|  | if (match == null) return line; | 
|  | return removeCommentary(removeCommand(line, r'commentary', match.start)); | 
|  | } | 
|  |  | 
|  | /// Removes {}-balanced '\rationale{..}' commands from [line]. | 
|  | String removeRationale(String line) { | 
|  | var match = rationaleRE.firstMatch(line); | 
|  | if (match == null) return line; | 
|  | return removeRationale(removeCommand(line, r'rationale', match.start)); | 
|  | } | 
|  |  | 
|  | /// Removes {}-balanced '\commentary{..}' and '\rationale{..}' | 
|  | /// commands from [line], then normalizes its white-space. | 
|  | String simplifyLine(String line) { | 
|  | var simplerLine = removeCommentary(line); | 
|  | simplerLine = removeRationale(simplerLine); | 
|  | simplerLine = normalizeWhitespace(simplerLine); | 
|  | return simplerLine; | 
|  | } | 
|  |  | 
|  | // ---------------------------------------------------------------------- | 
|  | // Recognition of line blocks, insertion of block hash into \LMHash{}. | 
|  |  | 
|  | final latexArgumentRE = RegExp(r'\{.*\}'); | 
|  |  | 
|  | String cleanupLine(String line) => | 
|  | cutRegexp(line, commentRE, startOffset: 1).trimRight(); | 
|  |  | 
|  | /// Returns concatenation of all lines from [startIndex] in [lines] until | 
|  | /// a hash block terminator is encountered or [nextIndex] reached (if so, | 
|  | /// the line lines[nextIndex] itself is not included); each line is cleaned | 
|  | /// up using [cleanupLine], and " " is inserted between the lines gathered. | 
|  | String gatherLines(List<String> lines, int startIndex, int nextIndex) => lines | 
|  | .getRange(startIndex, nextIndex) | 
|  | .takeWhile(isntHashBlockTerminator) | 
|  | .map(cleanupLine) | 
|  | .join(' '); | 
|  |  | 
|  | /// Computes the hash value for the line block starting at [startIndex] | 
|  | /// in [lines], stopping just before [nextIndex].  SIDE EFFECT: | 
|  | /// Outputs the simplified text and its hash value to [listSink]. | 
|  | List<int> computeHashValue( | 
|  | List<String> lines, int startIndex, int nextIndex, IOSink listSink) { | 
|  | final gatheredLine = gatherLines(lines, startIndex, nextIndex); | 
|  | final simplifiedLine = simplifyLine(gatheredLine); | 
|  | listSink.write('  % $simplifiedLine\n'); | 
|  | var digest = sha1.convert(utf8.encode(simplifiedLine)); | 
|  | return digest.bytes; | 
|  | } | 
|  |  | 
|  | String computeHashString( | 
|  | List<String> lines, int startIndex, int nextIndex, IOSink listSink) => | 
|  | hex.encode(computeHashValue(lines, startIndex, nextIndex, listSink)); | 
|  |  | 
|  | /// Computes and adds hashes to \LMHash{} lines in [lines] (which must be on the | 
|  | /// line numbers specified in [hashEvents]), and emits sectioning markers and | 
|  | /// hash values to [listSink], along with "comments" containing the simplified | 
|  | /// text (using the format `'  % <text>'`, where the text is one, long line, for | 
|  | /// easy grepping etc.). | 
|  | void addHashMarks( | 
|  | List<String> lines, List<HashEvent> hashEvents, IOSink listSink) { | 
|  | for (var hashEvent in hashEvents) { | 
|  | if (hashEvent is HashMarkerEvent) { | 
|  | var start = hashEvent.startLineNumber; | 
|  | var end = hashEvent.endLineNumber; | 
|  | final hashValue = computeHashString(lines, start + 1, end!, listSink); | 
|  | lines[start] = lines[start].replaceAll(latexArgumentRE, '{$hashValue}'); | 
|  | listSink.write('  $hashValue\n'); | 
|  | } else if (hashEvent is HashLabelEvent) { | 
|  | listSink.write('${hashEvent.labelText}\n'); | 
|  | } | 
|  | } | 
|  | } | 
|  |  | 
|  | /// Transforms LaTeX input to LaTeX output plus hash value list file. | 
|  | void main(List<String> args) { | 
|  | if (args.length != 3) { | 
|  | print('Usage: addlatexhash.dart <input-file> <output-file> <list-file>'); | 
|  | throw 'Received ${args.length} arguments, expected three'; | 
|  | } | 
|  |  | 
|  | // Get LaTeX source. | 
|  | var inputFile = File(args[0]); | 
|  | assert(inputFile.existsSync()); | 
|  | var lines = inputFile.readAsLinesSync(); | 
|  |  | 
|  | // Will hold LaTeX source with normalized spacing etc., plus hash values. | 
|  | var outputFile = File(args[1]); | 
|  |  | 
|  | // Will hold hierarchical list of hash values. | 
|  | var listFile = File(args[2]); | 
|  | var listSink = listFile.openWrite(); | 
|  |  | 
|  | // Perform single-line normalization. | 
|  | var inDartCode = false; | 
|  | var normalizedLines = <String>[]; | 
|  |  | 
|  | for (var line in lines) { | 
|  | if (sispIsDartBegin(line)) { | 
|  | inDartCode = true; | 
|  | } else if (sispIsDartEnd(line)) { | 
|  | inDartCode = false; | 
|  | } | 
|  | if (inDartCode) { | 
|  | normalizedLines.add(sispNormalize('$line\n')); | 
|  | } else { | 
|  | normalizedLines.add(normalize('$line\n')); | 
|  | } | 
|  | } | 
|  |  | 
|  | // Perform multi-line normalization. | 
|  | normalizedLines = multilineNormalize(normalizedLines); | 
|  |  | 
|  | // Insert hash values. | 
|  | var hashEvents = findHashEvents(normalizedLines); | 
|  | addHashMarks(normalizedLines, hashEvents, listSink); | 
|  |  | 
|  | // Produce/finalize output. | 
|  | outputFile.writeAsStringSync(normalizedLines.join()); | 
|  | listSink.close(); | 
|  | } |