R/patchLog.R

Defines functions readConcords myNormalizePath `mysubstr<-` patchLog

Documented in patchLog

# Patch a latex log file.  This borrows from logParser.js from TeXWorks,
# which had this author information and a GPL license.

#// TeXworksScript
#// Title: Errors, warnings, badboxes
#// Description: Looks for errors, warnings or badboxes in the LaTeX terminal output
#// Author: Jonathan Kew, Stefan Löffler, Antonio Macrì, Henrik Skov Midtiby
#// Version: 0.8.2
#// Date: 2012-03-26

patchLog <- function(f, newname=f, concords = NULL, max_print_line = 79, Cstyle = FALSE) {

  force(newname)
  lines <- readLines(f)

  # These patterns recognize all errors generated with \errmessage,
  # that is, starting with "!" and containing "l.\d+".
  # The macro \GenericError uses \errmessage internally.
  # Macros \@latex@error and \(Class|Package)Error use \GenericError.

  fileregexp <- '(?:"((?:\\./|/|\\.\\\\|[a-zA-Z]:\\\\|[a-zA-Z]:/|\\\\\\\\)(?:[^"]|$)+)"|((?:\\./|/|\\.\\\\|[a-zA-Z]:\\\\|[a-zA-Z]:/|\\\\\\\\)[^ ()$]+))'
  errormsg <- regexpr("^!\\s+", lines)
  errormsgC <- regexpr(paste0("^", fileregexp, ":(\\d+):\\s"), lines, perl = TRUE)
  if (is.na(Cstyle))
    Cstyle <- (sum(errormsgC != -1) > sum(errormsg != -1))

  if (Cstyle) {
    errormsg <- errormsgC
    errorline <- errormsgC
    errormsgbegin <- regexpr(paste0("^", fileregexp, ":(\\d+):\\s.*begin.*on input line (\\d+)"), lines, perl = TRUE)
  } else {
    # We capture extra text in errorline and errormsgbegin so that C style and default
    # style end up with the line number in the same location.
    errorline <- regexpr("()l(\\.)(\\d+)", lines, perl = TRUE)
    errormsgbegin <- regexpr("^(!\\s).*(begin).*(on input line) (\\d+)", lines, perl = TRUE)
  }
  # We don't actually patch Warning messages, but here's the regex from
  # logParser.js to do so if we ever want to:
  # warnings <- regexpr("^(?:Class|Package|LaTeX) ([^\\\\s]+) Warning: .+\\n(?:\\\\(\\\\1\\\\)\\\\s.+\\n)*(?:.*\\\\.\\n)?",
  #		      lines, perl = TRUE)
  badbox <- regexpr("^(?:Under|Over)full \\\\[hv]box\\s*\\([^)]+\\) in paragraph at lines (\\d+)--(\\d+)$",
                    lines, perl = TRUE)

  filestart <- gregexpr(paste0('\\(', fileregexp), lines, perl = TRUE)
  lineswithfile <- which(sapply(filestart, function(x) length(x) > 1 || x != -1))
  filestartline <- rep.int(lineswithfile, sapply(filestart[lineswithfile], length))
  filestartcol <- unlist(lapply(filestart[lineswithfile], function(x) attr(x, "capture.start")[,2]))
  filenamelen <- unlist(lapply(filestart[lineswithfile], function(x) attr(x, "capture.length")[,2]))
  filename <- substr(lines[filestartline], filestartcol, filestartcol + filenamelen - 1)

  realnames <- unique(filename)
  realnames <- realnames[file.exists(realnames)]

  keep <- filename %in% realnames
  filestartline <- filestartline[keep]
  filestartcol <- filestartcol[keep]
  filenamelen <- filenamelen[keep]
  filename <- myNormalizePath(filename[keep])

  openparen <- gregexpr("(", lines, fixed = TRUE)
  closeparen <- gregexpr(")", lines, fixed = TRUE)

  # Need to ignore the text in badbox warnings which may contain parens
  errortext <- which(badbox != -1) + 1
  lastline <- errortext
  repeat {
    continued <- nchar(lines[lastline], type="bytes") >= 79
    lastline <- setdiff(lastline[continued] + 1, errortext)
    if (!length(lastline)) break
    errortext <- union(errortext, lastline)
  }
  # similarly, text may show up in errorlines
  errortext <- union(errortext, which(errorline != -1))

  openparen[errortext] <- list(-1)
  closeparen[errortext] <- list(-1)

  lineswithopen <- which(sapply(openparen, function(x) length(x) > 1 || x != -1))
  lineswithclose <- which(sapply(closeparen, function(x) length(x) > 1 || x != -1))

  openlines <- rep.int(lineswithopen, sapply(openparen[lineswithopen], length))
  closelines <- rep.int(lineswithclose, sapply(closeparen[lineswithclose], length))

  opencols <- do.call(c, openparen[lineswithopen])
  closecols <- do.call(c, closeparen[lineswithclose])

  parenlocs <- rbind( cbind(openlines, opencols, 1), cbind(closelines, closecols, -1) )
  o <- order(parenlocs[,1], parenlocs[,2])
  parenlocs <- parenlocs[o,]
  parenlines <- parenlocs[,1]
  parencols <- parenlocs[,2]

  #parendepth is the parenthesis depth just after that character
  parendepth <- cumsum(parenlocs[,3])

  fileineffect <- character(length(lines))
  inds <- seq_along(parendepth)

  for (i in seq_along(filename)) {
    j <- which(parenlines == filestartline[i] & parencols + 1 == filestartcol[i])
    depth <- parendepth[j]
    popped <- which(inds > j & parendepth == depth - 1)

    fileendline <- if (length(popped)) parenlines[min(popped)] else length(lines)
    fileineffect[filestartline[i]:fileendline] <- filename[i]
  }

  if (!length(concords)) { # The run failed, so get them from the source
    concordance <- grepl("-concordance.tex$", filename)
    if (any(concordance))
      concords <- readConcords( unique(filename[concordance]) )
  }

  if (length(concords)) {
    # Now start patching.  Need to patch last thing on a line first.

    names(concords) <- myNormalizePath(names(concords), mustWork = FALSE)
    patchable <- fileineffect %in% names(concords)

    # First, the badbox lines
    dofix <- which(patchable & (badbox != -1))
    if (length(dofix)) {
      firststart <- attr(badbox, "capture.start")[dofix,1]
      firststop <- firststart + attr(badbox, "capture.length")[dofix,1] - 1
      secondstart <- attr(badbox, "capture.start")[dofix, 2]
      secondstop  <- secondstart + attr(badbox, "capture.length")[dofix, 2] - 1
      first <- as.numeric(substr(lines[dofix], firststart, firststop))
      second <- as.numeric(substr(lines[dofix], secondstart, secondstop))
      for (f in unique(fileineffect[dofix])) {
        thisfile <- fileineffect[dofix] == f
        first[thisfile] <- concords[[f]]$concord[first[thisfile]]
        second[thisfile] <- concords[[f]]$concord[second[thisfile]]
      }
      mysubstr(lines[dofix], secondstart, secondstop) <- as.character(second)
      mysubstr(lines[dofix], firststart, firststop) <- as.character(first)
    }

    # Now, the error messages talking about missing \end
    dofix <- which(patchable & (errormsgbegin != -1))
    if (length(dofix)) {
      start <- attr(errormsgbegin, "capture.start")[dofix, 4]
      stop <- start + attr(errormsgbegin, "capture.length")[dofix, 4] - 1
      num <- as.numeric(substr(lines[dofix], start, stop))
      for (f in unique(fileineffect[dofix])) {
        thisfile <- fileineffect[dofix] == f
        num[thisfile] <- concords[[f]]$concord[num[thisfile]]
      }
      mysubstr(lines[dofix], start, stop) <- as.character(num)
    }

    # Now, the other error lines
    dofix <- which(patchable & (errorline != -1))
    if (length(dofix)) {
      start <- attr(errorline, "capture.start")[dofix, 3]
      stop <- start + attr(errorline, "capture.length")[dofix, 3] - 1
      num <- as.numeric(substr(lines[dofix], start, stop))
      for (f in unique(fileineffect[dofix])) {
        thisfile <- fileineffect[dofix] == f
        num[thisfile] <- concords[[f]]$concord[num[thisfile]]
      }
      mysubstr(lines[dofix], start, stop) <- as.character(num)
      if (Cstyle) {
        cap <- ifelse(attr(errorline, "capture.length")[dofix, 1] > 0, 1, 2)
        start <- attr(errorline, "capture.start")[cbind(dofix, cap)]
        stop <- start + attr(errorline, "capture.length")[cbind(dofix, cap)] - 1
        f <- myNormalizePath(substr(lines[dofix], start, stop))
        mysubstr(lines[dofix], start, stop) <-
          sapply(concords[f], function(x) x$newsrc)
      }
    }

    # And finally, the filenames
    dofix <- filename %in% names(concords)
    if (any(dofix)) {
      start <- filestartcol[dofix]
      stop <- start + filenamelen[dofix] - 1
      withPath <- function(s) {
        okay <- grepl("[/\\]", s)
        s[!okay] <- paste0("./", s[!okay])
      }
      mysubstr(lines[filestartline[dofix]], start, stop) <- sapply(concords[filename[dofix]],
                                                                   function(x) withPath(x$newsrc))
    }
  }
  writeLines(lines, newname)
  invisible(concords)
}

`mysubstr<-` <- function(x, start, stop, value)
  paste0(substr(x, 1, start-1), value, substr(x, stop+1, nchar(x)))

myNormalizePath <- function(f, mustWork = NA) {
  filename <- normalizePath(f, mustWork = mustWork)
  if (.Platform$OS.type == "windows") {
    upperdrive <- grepl("^[[:upper:]]:", filename)
    substr(filename[upperdrive], 1,1) <- tolower(substr(filename[upperdrive], 1,1))
  }
  filename
}

readConcords <- function(files) {
  concords <- character(length(files))
  for (i in seq_along(files)) {
    lines <- readLines(files[i])
    if (grepl("\\Sconcordance{", lines[1], fixed=TRUE)) {
      lines[1] <- sub("\\Sconcordance{", "", lines[1], fixed = TRUE)
      lines[length(lines)] <- sub("}$", "", lines[length(lines)])
      lines <- sub("%$", "", lines)
      concords[i] <- paste(lines, collapse="")
    }
  }
  parseConcords(concords)
}

Try the patchDVI package in your browser

Any scripts or data that you put into this service are public.

patchDVI documentation built on March 31, 2023, 11:34 p.m.