#  File src/library/tools/R/Rd2txt.R
#  Part of the R package, https://www.R-project.org
#
#  Copyright (C) 1995-2026 The R Core Team
#
#  This program is free software; you can redistribute it and/or modify
#  it under the terms of the GNU General Public License as published by
#  the Free Software Foundation; either version 2 of the License, or
#  (at your option) any later version.
#
#  This program is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#  GNU General Public License for more details.
#
#  A copy of the GNU General Public License is available at
#  https://www.R-project.org/Licenses/

## This stops on
##  unrecognized tag
##  \\tabular format must be simple text
##  too many columns for format
##  invalid markup in \[S3]method
##  "Tag ", tag, " not expected in code block"

tabExpand <- function(x) {
    srcref <- attr(x, "srcref")
    start <- if(is.null(srcref)) 0L else srcref[5L] - 1L
    .Call(C_doTabExpand, x, start)
}

Rd2txt_options <- local({
    opts <- list(width = 80L,
                 minIndent = 10L,
    	         extraIndent = 4L,
    	         sectionIndent = 5L,
    	         sectionExtra = 2L,
    	         itemBullet = "* ",
    	         enumFormat = function(n) sprintf("%d. ", n),
                 descStyle = "linebreak",
                 showURLs = FALSE,
                 code_quote = TRUE,
                 underline_titles = TRUE)
    function(...) {
        args <- list(...)
        if (!length(args))
            return(opts)
        else {
            if (is.list(args[[1L]])) args <- args[[1L]]
            result <- opts[names(args)]
            names(result) <- names(args)
            opts[names(args)] <<- args
            invisible(result)
        }
    }
})

math_replacements <- matrix(c(
    "Alpha",   "\u391",  "Alpha",   "&Alpha;",
    "Beta",    "\u392",  "Beta",    "&Beta;",
    "Gamma",   "\u393",  "Gamma",   "&Gamma;",
    "Delta",   "\u394",  "Delta",   "&Delta;",
    "Epsilon", "\u395",  "Epsilon", "&Epsilon;",
    "Zeta",    "\u396",  "Zeta",    "&Zeta;",
    "Eta",     "\u397",  "Eta",     "&Eta;",
    "Theta",   "\u398",  "Theta",   "&Theta;",
    "Iota",    "\u399",  "Iota",    "&Iota;",
    "Kappa",   "\u39a",  "Kappa",   "&Kappa;",
    "Lambda",  "\u39b",  "Lambda",  "&Lambda;",
    "Mu",      "\u39c",  "Mu",      "&Mu;",
    "Nu",      "\u39d",  "Nu",      "&Nu;",
    "Xi",      "\u39e",  "Xi",      "&Xi;",
    "Omicron", "\u39f",  "Omicron", "&Omicron;",
    "Pi",      "\u3a0",  "Pi",      "&Pi;",
    "Rho",     "\u3a1",  "Rho",     "&Rho;",
    "Sigma",   "\u3a3",  "Sigma",   "&Sigma;",
    "Tau",     "\u3a4",  "Tau",     "&Tau;",
    "Upsilon", "\u3a5",  "Upsilon", "&Upsilon;",
    "Phi",     "\u3a6",  "Phi",     "&Phi;",
    "Chi",     "\u3a7",  "Chi",     "&Chi;",
    "Psi",     "\u3a8",  "Psi",     "&Psi;",
    "Omega",   "\u3a9",  "Omega",   "&Omega;",
    "alpha",   "\u3b1",  "alpha",   "&alpha;",
    "beta",    "\u3b2",  "beta",    "&beta;",
    "gamma",   "\u3b3",  "gamma",   "&gamma;",
    "delta",   "\u3b4",  "delta",   "&delta;",
    "epsilon", "\u3b5",  "epsilon", "&epsilon;",
    "zeta",    "\u3b6",  "zeta",    "&zeta;",
    "eta",     "\u3b7",  "eta",     "&eta;",
    "theta",   "\u3b8",  "theta",   "&theta;",
    "iota",    "\u3b9",  "iota",    "&iota;",
    "kappa",   "\u3ba",  "kappa",   "&kappa;",
    "lambda",  "\u3bb",  "lambda",  "&lambda;",
    "mu",      "\u3bc",  "mu",      "&mu;",
    "nu",      "\u3bd",  "nu",      "&nu;",
    "xi",      "\u3be",  "xi",      "&xi;",
    "omicron", "\u3bf",  "omicron", "&omicron;",
    "pi",      "\u3c0",  "pi",      "&pi;",
    "rho",     "\u3c1",  "rho",     "&rho;",
    "sigma",   "\u3c3",  "sigma",   "&sigma;",
    "tau",     "\u3c4",  "tau",     "&tau;",
    "upsilon", "\u3c5",  "upsilon", "&upsilon;",
    "phi",     "\u3c6",  "phi",     "&phi;",
    "chi",     "\u3c7",  "chi",     "&chi;",
    "psi",     "\u3c8",  "psi",     "&psi;",
    "omega",   "\u3c9",  "omega",   "&omega;",
    "sum",     "\u2211", "sum",     "&sum;",
    "prod",    "\u220f", "prod",    "&prod;",
    "sqrt",    "\u221a", "sqrt",    "&radic;",
    "dots",    "\u2026", "...",     "&hellip;",
    "ldots",   "\u2026", "...",     "&hellip;",
    "le",      "\u2264", "<=",      "&le;",
    "leq",     "\u2264", "<=",      "&le;",
    "ge",      "\u2265", ">=",      "&ge;",
    "geq",     "\u2265", ">=",      "&ge;",
    "ne",      "\u2260", "!=",      "&ne;",
    "neq",     "\u2260", "!=",      "&ne;",
    "infty",   "\u221e", "Inf",     "&infin;",
    "left",    "",       "",        "",
    "right",   "",       "",        ""
), ncol = 4, byrow = TRUE, dimnames = list(NULL, c("name", "fancy", "ascii", "html")))

transformMethod <- function(i, blocks, Rdfile) {
    editblock <- function(block, newtext)
    	list(tagged(newtext,
                    attr(block, "Rd_tag"),
                    attr(block, "srcref")))

    # Most of the internal functions below are more like macros
    # than functions; they mess around with these variables:

    chars <- NULL
    char <- NULL
    j <- NULL

    findOpen <- function(i) {
    	j <- i
    	char <- NULL
    	while (j < length(blocks)) {
    	    j <- j + 1L
    	    tag <- attr(blocks[[j]], "Rd_tag")
    	    if (tag == "RCODE") {

    	        # FIXME:  This search and the ones below will be fooled
    	        # by "#" comments

    	    	chars <- strsplit(blocks[[j]], "")[[1]]
    		parens <- cumsum( (chars == "(") - (chars == ")") )
    		if (any(parens > 0)) {
		    char <- which.max(parens > 0)
    	   	    break
    	   	}
    	    }
    	}
    	if (is.null(char))
    	    stopRd(block, Rdfile, sprintf("no parenthesis following %s", blocktag))
    	chars <<- chars
    	char <<- char
    	j <<- j
    }

    findComma <- function(i) {
	j <- i
	level <- 1L
	char <- NULL
	while (j < length(blocks)) {
	    j <- j + 1L
	    tag <- attr(blocks[[j]], "Rd_tag")
	    if (tag == "RCODE") {
		chars <- strsplit(blocks[[j]], "")[[1]]
		parens <- level + cumsum( (chars == "(") - (chars == ")") )
		if (any(parens == 1 & chars == ",")) {
		    char <- which.max(parens == 1 & chars == ",")
		    break
		}
		if (any(parens == 0))
		    break
		level <- parens[length(parens)]
	    }
	}
	if (is.null(char))
	    stopRd(block, Rdfile, sprintf("no comma in argument list following %s", blocktag))
        chars <<- chars
        char <<- char
        j <<- j
    }


    findClose <- function(i) {
        j <- i
    	level <- 1L
    	char <- NULL
    	while (j < length(blocks)) {
    	    j <- j + 1L
    	    tag <- attr(blocks[[j]], "Rd_tag")
    	    if (tag == "RCODE") {
    	    	chars <- strsplit(blocks[[j]], "")[[1]]
    	    	parens <- level + cumsum( (chars == "(") - (chars == ")") )
    	    	if (any(parens == 0)) {
    	    	    char <- which(parens == 0)[1]
    	    	    break
    	    	}
    	    	level <- parens[length(parens)]
    	    }
    	}
    	if (is.null(char))
    	    stopRd(block, Rdfile, sprintf("no closing parenthesis following %s", blocktag))
	chars <<- chars
        char <<- char
        j <<- j
    }

    rewriteBlocks <- function()
    	c(blocks[seq_len(j-1L)],
    	            editblock(blocks[[j]],
    	                      paste(chars[seq_len(char)], collapse="")),
    	            if (char < length(chars))
    	                editblock(blocks[[j]],
    	                          paste(chars[-seq_len(char)], collapse="")),
	            if (j < length(blocks)) blocks[-seq_len(j)])

    deleteBlanks <- function() {
	while (char < length(chars)) {
	    if (chars[char + 1] == " ") {
	    	char <- char + 1
	    	chars[char] <- ""
	    } else
	    	break
	}
	char <<- char
	chars <<- chars
    }

    block <- blocks[[i]]
    blocktag <- attr(block, "Rd_tag")
    srcref <- attr(block, "srcref")
    class <- block[[2L]] # or signature
    generic <- as.character(block[[1L]])
    default <- as.character(class) == "default"

    if(generic %in% c("[", "[[", "$")) {
	## need to assemble the call by matching parens in RCODE
	findOpen(i) # Sets chars, char and j
	chars[char] <- ""
	blocks <- c(blocks[seq_len(j-1L)],
	            editblock(blocks[[j]],
	                      paste(chars[seq_len(char)], collapse="")),
	            if (char < length(chars))
	                editblock(blocks[[j]],
	                          paste(chars[-seq_len(char)], collapse="")),
	            if (j < length(blocks)) blocks[-seq_len(j)])

	findComma(j) # Sets chars, char and j
	chars[char] <- generic
	# Delete blanks after the comma
	deleteBlanks()
	blocks <- rewriteBlocks()

	findClose(j)
	# Edit the closing paren
	chars[char] <- switch(generic,
		"[" = "]",
		"[[" = "]]",
		"$" = "")
	blocks[j] <- editblock(blocks[[j]],
	                         paste(chars, collapse=""))

	methodtype <- if (grepl("<-", blocks[[j]])) "replacement " else ""
    } else if(grepl(sprintf("^%s$",
			   paste(c("\\+", "\\-", "\\*",
				   "\\/", "\\^", "<=?",
				   ">=?", "!=?", "==",
				   "\\&", "\\|", "!",
				   "\\%[[:alnum:][:punct:]]*\\%"),
				 collapse = "|")),
		   generic)) {
        ## Binary operators and unary '!'.
	findOpen(i)

	if (generic != "!") {
            chars[char] <- ""
            blocks <- rewriteBlocks()
	    findComma(j)
	    chars[char] <- paste0(" ", generic, " ")
	    # Delete blanks after the comma
	    deleteBlanks()
	    blocks <- rewriteBlocks()
	} else {
            chars[char] <- "!"
	    blocks <- rewriteBlocks()
        }

	findClose(j)
	chars[char] <- ""
	blocks[j] <- editblock(blocks[[j]],
                               paste(chars, collapse=""))

	methodtype <- ""
    } else {
        findOpen(i)
	chars[char] <- paste0(generic, "(")
	blocks <- rewriteBlocks()
	findClose(j)
	methodtype <- if (grepl("<-", blocks[[j]])) "replacement " else ""
    }

    if (blocktag == "\\S4method") {
        ## some signatures are very long.
        blocks <- if(nchar(class) > 50L) {
            cl <- paste0("'", as.character(class), "'")
            if(nchar(cl) > 70L) {
                cl <- strsplit(cl, ",")[[1L]]
                ncl <- length(cl)
                cl[-ncl] <- paste0(cl[-ncl], ",")
                cl[-1L] <- paste0("  ", cl[-1L])
            }
            cl <- paste("##", cl, collapse="\n")
            c( blocks[seq_len(i-1L)],
              list(tagged(paste0("## S4 ", methodtype,
                                 "method for signature \n"),
                          "RCODE", srcref)),
              list(tagged(cl, "TEXT", srcref)),
              list(tagged("\n", "RCODE", srcref)),
              blocks[-seq_len(i)] )
        } else
            c( blocks[seq_len(i-1L)],
              list(tagged(paste0("## S4 ", methodtype,
                                 "method for signature '"),
                          "RCODE", srcref)),
              class,
              list(tagged("'\n", "RCODE", srcref)),
              blocks[-seq_len(i)] )
    } else if (default)
    	blocks <- c( blocks[seq_len(i-1)],
                     list(tagged(paste0("## Default S3 ", methodtype,
                                        "method:\n"),
                                 "RCODE", srcref)),
    		     blocks[-seq_len(i)] )
    else
    	blocks <- c( blocks[seq_len(i-1)],
                     list(tagged(paste0("## S3 ", methodtype,
                                        "method for class '"),
                                 "RCODE", srcref)),
		     class,
		     list(tagged("'\n", "RCODE", srcref)),
		     blocks[-seq_len(i)] )
    blocks
}# transformMethod()

Rd2txt <-
    function(Rd, out="", package = "", defines=.Platform$OS.type,
             stages = "render", outputEncoding = "",
             fragment = FALSE, options, ...)
{

    ## we need to keep track of where we are.
    buffer <- character()	# Buffer not yet written to con
    				# Newlines have been processed, each line in buffer is
    				# treated as a separate input line (but may be wrapped before output)
    linestart <- TRUE		# At start of line?
    indent <- 0L		# Default indent
    wrapping <- TRUE		# Do word wrap?
    keepFirstIndent <- FALSE	# Keep first line indent?
    dropBlank <- FALSE		# Drop initial blank lines?
    haveBlanks <- 0L		# How many blank lines have just been written?
    enumItem <- 0L		# Last enumeration item number
    inEqn <- FALSE		# Should we do edits needed in an eqn?
    sectionLevel <- 0		# How deeply nested within sections/subsections

    saveOpts <- Rd2txt_options()
    on.exit(Rd2txt_options(saveOpts))# Rd files may change these, so restore them
    				     # whether or not the caller set them.
    if (!missing(options)) Rd2txt_options(options)

## these attempt to mimic pre-2.10.0 layout
    WIDTH <- 0.9 * Rd2txt_options()$width
    HDR_WIDTH <- WIDTH - 2L

    startCapture <- function() {
    	save <- list(buffer=buffer, linestart=linestart, indent=indent,
                     wrapping=wrapping, keepFirstIndent=keepFirstIndent,
                     dropBlank=dropBlank, haveBlanks=haveBlanks,
                     enumItem=enumItem, inEqn=inEqn)
    	buffer <<- character()
    	linestart <<- TRUE
    	indent <<- 0L
    	wrapping <<- TRUE
    	keepFirstIndent <<- FALSE
    	dropBlank <<- FALSE
    	haveBlanks <<- 0L
    	enumItem <<- 0L
    	inEqn <<- FALSE
    	save
    }

    endCapture <- function(saved) {
    	result <- buffer
    	buffer <<- saved$buffer
    	linestart <<- saved$linestart
    	indent <<- saved$indent
    	wrapping <<- saved$wrapping
    	keepFirstIndent <<- saved$keepFirstIndent
    	dropBlank <<- saved$dropBlank
    	haveBlanks <<- saved$haveBlanks
    	enumItem <<- saved$enumItem
    	inEqn <<- saved$inEqn
    	result
    }

    ## for efficiency
    asUTF8 <- outputEncoding == "UTF-8" ||
        (outputEncoding == "" && l10n_info()[["UTF-8"]])
    WriteLines <- if(asUTF8) {
        function(x, con, outputEncoding, ...)
            writeLines(x, con, useBytes = TRUE, ...)
    } else {
        function(x, con, outputEncoding, ...) {
            x <- iconv(x, "UTF-8", outputEncoding, sub="byte", mark=FALSE)
            writeLines(x, con, useBytes = TRUE, ...)
        }
    }

    ## Use display widths as used by cat not print.
    ## This may receive length(x) > 1 lines.
    ## Optionally format only if the input can be collapsed into a single line.
    frmt <- function(x, justify = "left", width = 0L, collapsed = FALSE) {
        justify <- match.arg(justify, c("left", "right", "centre", "none"))
        if(justify == "none" || !length(x))
            return(x)
        if(collapsed) { # also trims single-line input
            y <- paste0(trim(x), collapse = " ")
            w <- nchar(y, "width")
            if(w < width)
                x <- y
            else return(x)
        } else {
            w <- nchar(x, "width")
        }
        if(any(w < width)) {
            excess <- pmax(0, width - w)
            left <- right <- 0L
            if(justify == "left") right <- excess
            else if(justify == "right")  left <- excess
            else if(justify == "centre") {
                left <- excess %/% 2
                right <- excess-left
            }
            paste0(strrep(" ", left), x, strrep(" ", right))
        } else x
    }

    wrap <- function(doWrap = TRUE)
	if (doWrap != wrapping) { flushBuffer(); wrapping <<- doWrap }

    putw <- function(...)  { wrap(TRUE); put(...) }

    putf <- function(...)  { wrap(FALSE); put(...) }

    put <- function(...) {
        txt <- paste0(..., collapse="")
        trail <- endsWith(txt, "\n")
        # Convert newlines
        txt <- strsplit(txt, "\n", fixed = TRUE)[[1L]]
        if (dropBlank) {
            while(length(txt) && grepl("^[[:space:]]*$", txt[1L]))
            	txt <- txt[-1L]
            if (length(txt)) dropBlank <<- FALSE
        }
        if(!length(txt)) return()
        haveBlanks <<- 0

        if (linestart) buffer <<- c(buffer, txt)
        else if (length(buffer)) {
            buffer[length(buffer)] <<-
                paste0(buffer[length(buffer)], txt[1L])
            buffer <<- c(buffer, txt[-1L])
        }
        else buffer <<- txt
        linestart <<- trail
    }

    flushBuffer <- function() {
    	if (!length(buffer)) return()

    	if (wrapping) {
	    if (keepFirstIndent) {
		first <- nchar(psub1("[^ ].*", "", buffer[1L]))
		keepFirstIndent <<- FALSE
	    } else
		first <- indent

	    buffer <<- c(buffer, "")  # Add an extra blank sentinel
	    blankLines <- grep("^[[:space:]]*$", buffer)
	    result <- character()
	    start <- 1L
	    for (i in seq_along(blankLines)) {
		if (blankLines[i] > start) {
		    result <- c(result,
                                strwrap(paste(buffer[start:(blankLines[i]-1L)],
                                              collapse = " "),
                                        WIDTH, indent = first, exdent = indent))
		    first <- indent
                }
                result <- c(result, "")
		start <- blankLines[i]+1L
	    }
            ## we want to collapse multiple blank lines when wrapping
            ## and to remove the sentinel (which we need to do first or
            ## we will drop a single blank line)
            buffer <<- result[-length(result)]
            empty <- !nzchar(buffer)
            drop <- empty & c(FALSE, empty[-length(empty)])
            buffer <<- buffer[!drop]
	} else {  # Not wrapping
	    if (keepFirstIndent) {
		if (length(buffer) > 1L)
		    buffer[-1L] <<- paste0(strrep(" ", indent), buffer[-1L])
		keepFirstIndent <- FALSE
	    } else
		buffer <<- paste0(strrep(" ", indent), buffer)
	}

    	if (length(buffer)) WriteLines(buffer, con, outputEncoding)
    	buffer <<- character()
    	linestart <<- TRUE
    }

    unicode_symbols <- asUTF8 &&
        ## disable in title (see .Rd_get_text) to match existing *-Ex.Rout.save
        !isFALSE(Rd2txt_options()$unicode_symbols)

    if(unicode_symbols && !isFALSE(getOption("useFancyQuotes"))) {
    	LSQM <- "\u2018"                # Left single quote
    	RSQM <- "\u2019"                # Right single quote
    	LDQM <- "\u201c"                # Left double quote
    	RDQM <- "\u201d"                # Right double quote
    } else {
        LSQM <- RSQM <- "'"
        LDQM <- RDQM <- '"'
    }

    trim <- function(x) {
        x <- psub1("^\\s*", "", x)
        psub1("\\s*$", "", x)
    }

    ## underline via backspacing
    txt_header <- function(header) {
        opts <- Rd2txt_options()
        header <- paste(strwrap(header, WIDTH), collapse="\n")
        if (opts$underline_titles) {
            letters <- strsplit(header, "", fixed = TRUE)[[1L]]
            isaln <- grep("[[:alnum:]]", letters)
            letters[isaln] <- paste0("_\b", letters[isaln])
            paste(letters, collapse = "")
        } else header
    }

    unescape <- function(x) {
        if (unicode_symbols) {
            x <- fsub("---", "\u2014", x)
            x <- fsub("--",  "\u2013", x)
        } else {
            x <- psub("(---|--)", "-", x)
        }
        x
    }

    writeCode <- function(x) {
        txt <- as.character(x)
        if(inEqn) txt <- txt_eqn(txt)
        txt <- fsub('"\\{"', '"{"', txt)
        put(txt)
    }

    ## Strip a pending blank line
    stripBlankLine <- function() {
        n <- length(buffer)
        strip <- n > 0L && grepl("^[[:blank:]]*$", buffer[n])
        if (strip) buffer <<- buffer[-n]
        strip
    }
    ## Strip pending blank lines, then add n new ones.
    ## (Currently not used with n > 1, which only works if not 'wrapping'.)
    blankLine <- function(n = 1L) {
    	while (stripBlankLine()) NULL
	flushBuffer()
	if (n > haveBlanks) {
	    buffer <<- rep_len("", n - haveBlanks)
	    flushBuffer()
	    haveBlanks <<- n
	}
	dropBlank <<- TRUE
    }

    txt_eqn <- function(x) {
        replacement <- if (unicode_symbols) "fancy" else "ascii"
        rx <- paste0("\\\\(",
                     paste(math_replacements[,"name"], collapse = "|"),
                     ")(?![a-zA-Z])")
        m <- gregexec(rx, x, perl = TRUE)
        ii <- lapply(regmatches(x, m),
                     function(mm) if (length(mm)) match(mm[2,], math_replacements[,"name"]))
        regmatches(x, gregexpr(rx, x, perl = TRUE)) <- lapply(ii, function(i) math_replacements[i, replacement])
        ## FIXME: are these needed?
        x <- psub("\\\\(bold|strong|emph|var)\\{([^}]*)\\}", "\\2", x)
        x <- psub("\\\\(code|samp)\\{([^}]*)\\}",
                  sprintf("%s\\2%s", LSQM, RSQM), x)
        x
    }

    wrappers <- list(
        "\\var"    = c("<", ">"),
        "\\bold"   = c("*", "*"),
        "\\strong" = c("*", "*"),
        "\\emph"   = c("_", "_")
    )
    writeWrapped <- function(block, tag) {
        if (isBlankRd(block))
            return() # skip \emph{} etc, consistent with HTML
    	LR <- wrappers[[tag]]
    	put(LR[1L])
    	writeContent(block, tag)
    	put(LR[2L])
    }

    writeDR <- function(block, tag) {
        if (length(block) > 1L) {
            putf('## Not run:\n')
            writeCodeBlock(block, tag)
            blankLine(0L)
            putf('## End(Not run)\n')
        } else {
            putf('## Not run: ')
            writeCodeBlock(block, tag)
        }
    }

    writeQ <- function(block, tag, quote=tag)
    {
        if (quote == "\\sQuote") {
            put(LSQM); writeContent(block, tag); put(RSQM)
        } else {
            put(LDQM); writeContent(block, tag); put(RDQM)
        }
    }

    writeBlock <- function(block, tag, blocktag) {
        switch(tag,
               UNKNOWN =,
               VERB =,
               RCODE = writeCode(tabExpand(block)),
               TEXT = if(blocktag == "\\command") putw(block) else putw(unescape(tabExpand(block))),
               USERMACRO =,
               "\\newcommand" =,
               "\\renewcommand" = {},
               COMMENT = {
                   if (linestart)
                       linestart <<- FALSE # eat subsequent \n also for non-indented comments
                   else stripBlankLine()   # drop indentation
               },
               LIST = writeContent(block, tag),
               "\\describe" = {
               	   blankLine(0L)
                   writeContent(block, tag)
                   blankLine()
               },
               "\\itemize"=,
               "\\enumerate"= {
               	   blankLine(0L)
                   enumItem0 <- enumItem
                   enumItem <<- 0L
                   indent0 <- indent
                   opts <- Rd2txt_options()
                   indent <<- max(opts$minIndent,
                              indent + opts$extraIndent)
                   dropBlank <<- TRUE
                   writeContent(block, tag)
                   blankLine()
                   indent <<- indent0
                   enumItem <<- enumItem0
               },
               "\\code"=,
               "\\command"=,
               "\\env"=,
               "\\file"=,
               "\\kbd"=,
               "\\option"=,
               "\\pkg"=,
               "\\samp" = {
                   opts <- Rd2txt_options()
                   if(opts$code_quote)
                       writeQ(block, tag, quote="\\sQuote")
                   else writeContent(block,tag)
               },
               "\\email" = {
                   # for legibility, do not URLencode: some use ". at ." etc
                   put("<mailto:", lines2str(as.character(block)), ">")
               },
               "\\url" = {
                   put("<", utils::URLencode(lines2str(as.character(block))), ">")
               },
               "\\href" = {
                   opts <- Rd2txt_options()
                   writeContent(block[[2L]], tag)
                   if (opts$showURLs)
  			put(" <", utils::URLencode(lines2str(as.character(block[[1L]]))), ">")
               },
               "\\Sexpr"= put(as.character.Rd(block, deparse=TRUE)),
               "\\abbr" =,
               "\\acronym" =,
               "\\dfn"= ,
               "\\special" = writeContent(block, tag),
               "\\var"=,
               "\\bold"=,
               "\\strong"=,
               "\\emph" = writeWrapped(block, tag),
               "\\cite" = writeQ(block, tag, quote = "\\sQuote"),
               "\\sQuote" =,
               "\\dQuote"= writeQ(block, tag) ,
               "\\preformatted"= {
                   blankLine()
                   wrap(FALSE)
                   writeCodeBlock(block, tag)
                   blankLine()
               },
               "\\verb"= {
                   writeContent(block[1L], tag)
                   if (length(block) > 1L) {
                       wrap(FALSE) # flush and keep subsequent linebreaks/formatting
                       writeContent(block[-1L], tag)
                   }
               },
               "\\linkS4class" =,
               "\\linkS4methods" =,
               "\\link" = writeContent(block, tag),
               "\\cr" = {
                   if (!length(buffer)) { # \cr\cr
                       dropBlank <<- FALSE
                       put("\n")
                   }
                   ## we want to print out what we have, and if
                   ## followed immediately by \n (as it usually is)
                   ## discard that.  This is not entirely correct,
                   ## but it is better than before ....
                   flushBuffer()
                   dropBlank <<- TRUE
                   },
               "\\dots" =,
               "\\ldots" = put("..."),
               "\\R" = put("R"),
               "\\enc" = {
                   ## Test to see if we can convert the encoded version
                   txt <- as.character(block[[1L]])
                   test <- iconv(txt, "UTF-8", outputEncoding, mark = FALSE)
                   txt <- if(!anyNA(test)) txt else as.character(block[[2L]])
                   put(txt)
               } ,
               "\\eqn" = {
                   block <- block[[length(block)]]
                   ## FIXME: treat 2 of 2 differently?
                   inEqn0 <- inEqn
                   inEqn <<- TRUE
                   dropBlank <<- TRUE
                   writeContent(block, tag)
                   inEqn <<- inEqn0
               },
               "\\deqn" = {
                   blankLine()
                   block <- block[[length(block)]]
                   save <- startCapture()
                   inEqn <<- TRUE
                   writeContent(block, tag)
                   eqn <- endCapture(save)
                   ## try collapsing into a single centred line (as in R < 4.4.0)
                   ## but only if the source block spans at most 3 lines
                   if(length(eqn) <= 3L)
                       eqn <- frmt(eqn, justify = "centre",
                                   width = WIDTH - indent, collapsed = TRUE)
                   putf(paste(eqn, collapse="\n"))
    		   blankLine()
               },
               "\\figure" = {
                   blankLine()
                   save <- startCapture()
                   writeContent(block[[length(block)]], tag)
                   alt <- endCapture(save)
                   if (length(alt)) {
                       alt <- frmt(alt, justify = "centre",
                                   width = WIDTH - indent)
                       putf(paste(alt, collapse = "\n"))
                       blankLine()
                   }
               },
               "\\tabular" = writeTabular(block),
               "\\subsection" = writeSection(block, tag),
               "\\if"=,
               "\\ifelse" =
                   if (testRdConditional("text", block, Rdfile))
               		writeContent(block[[2L]], tag)
               	   else if (tag == "\\ifelse")
               	   	writeContent(block[[3L]], tag),
               "\\out" = for (i in seq_along(block))
		   put(block[[i]]),
               stopRd(block, Rdfile, "Tag ", tag, " not recognized")
               )
    }

    writeTabular <- function(table) {
    	formats <- table[[1L]]
    	content <- table[[2L]]
    	if (length(formats) != 1L || RdTags(formats) != "TEXT")
    	    stopRd(table, Rdfile, "\\tabular format must be simple text")
    	formats <- strsplit(formats[[1L]], "", fixed = TRUE)[[1L]]
        tags <- RdTags(content)
        entries <- list()
        row <- 1L
        col <- 1L
        save <- startCapture()
        dropBlank <<- TRUE
        newEntry <- function() {
            entries <<- c(entries, list(list(text=trim(endCapture(save)),
	                   	             row=row, col=col)))
            save <<- startCapture()
            dropBlank <<- TRUE
        }
        for (i in seq_along(tags)) {
            switch(tags[i],
                  "\\tab" = {
                  	newEntry()
                   	col <- col + 1L
                   	if (col > length(formats))
                   	    stopRd(content[[i]], Rdfile,
                                   sprintf("too many columns for format '%s'",
                                           table[[1L]]))
                   },
                   "\\cr" = {
                   	newEntry()
                   	row <- row + 1L
			col <- 1L
                    },
                   writeBlock(content[[i]], tags[i], "\\tabular")
                   )
        }
        newEntry()
        endCapture(save)
        entries <- with(entries[[length(entries)]],
        	    {
                        if (!length(text) && col == 1L)
                            entries[-length(entries)]
                        else
                            entries
                    })
        if(!length(entries)) return()
        rows <- entries[[length(entries)]]$row
        cols <- max(vapply(entries, function(e) e$col, 1L))
        widths <- rep_len(0L, cols)
        lines <- rep_len(1L, rows)
        for (i in seq_along(entries)) {
            e <- entries[[i]]
            while(length(e$text) && !nzchar(e$text[length(e$text)])) {
            	e$text <- e$text[-length(e$text)]
            	entries[[i]] <- e
            }
            if (any(nzchar(e$text)))
            	widths[e$col] <- max(widths[e$col], max(nchar(e$text, "w")))
            ## NOTE: if an entry spanned multiple Rd lines, length(e$text) > 1.
            ## Whereas Rd lines are collapsed in both HTML (which auto-wraps)
            ## and PDF output, line breaks are preserved here (even though
            ## this is unusual for a LaTeX-like context) and the width
            ## is determined by the longest (trimmed) line of the column.
            lines[e$row] <- max(lines[e$row], length(e$text))
        }
        result <- matrix("", sum(lines), cols)
        for (i in seq_len(cols))
            result[, i] <- strrep(" ", widths[i])
        firstline <- c(1L, 1L+cumsum(lines))
        for (i in seq_along(entries)) {
            e <- entries[[i]]
            if(!length(e$text)) next
            ## FIXME: this is not right: it justifies strings as if
            ## they are escaped, so in particular \ takes two columns.
            text <- frmt(e$text, justify=formats[e$col], width=widths[e$col])
            for (j in seq_along(text))
            	result[firstline[e$row] + j - 1L, e$col] <- text[j]
        }
        blankLine()
        indent0 <- indent
        indent <<- indent + 1L
        for (i in seq_len(nrow(result))) {
            putf(paste0(" ", result[i,], " ", collapse=""))
# This version stripped leading blanks on the first line
#            for (j in seq_len(cols))
#            	putf(" ", result[i,j], " ")
            putf("\n")
        }
        blankLine()
        indent <<- indent0
    }

    writeCodeBlock <- function(blocks, blocktag)
    {
    	tags <- RdTags(blocks)
	i <- 0
	while (i < length(tags)) {
	    i <- i + 1
            block <- blocks[[i]]
            tag <- tags[i]
            switch(tag,
                   "\\method" =,
                   "\\S3method" =,
                   "\\S4method" = {
                   	blocks <- transformMethod(i, blocks, Rdfile)
                   	tags <- RdTags(blocks)
                   	i <- i - 1
                   },
                   UNKNOWN =,
                   VERB =,
                   RCODE =,
                   TEXT = writeCode(tabExpand(block)),
                   "\\donttest" =, "\\dontdiff" =,
                   "\\special" =,
                   "\\var" = writeCodeBlock(block, tag),
                   "\\dots" =, # \ldots is not really allowed
                   "\\ldots" = put("..."),
                   "\\dontrun"= writeDR(block, tag),
                   COMMENT = # skip over whole comment lines, only (as in Rd2ex)
                       if (attr(block, "srcref")[2L] == 1L) linestart <<- FALSE,
		   USERMACRO =,
		   "\\newcommand" =,
		   "\\renewcommand" =,
                   "\\dontshow" =,
                   "\\testonly" = {}, # do nothing
                   ## All the markup such as \emph
                   stopRd(block, Rdfile, "Tag ", tag,
                          " not expected in code block")
                   )
        }
    }

    writeContent <- function(blocks, blocktag) {
        itemskip <- FALSE
	tags <- RdTags(blocks)

	for (i in seq_along(tags)) {
            tag <- tags[i]
            block <- blocks[[i]]
            switch(tag,
                   "\\item" = {
                       switch(blocktag,
                              "\\describe"= {
                                  blankLine()
                                  save <- startCapture()
                                  dropBlank <<- TRUE
                                  writeContent(block[[1L]], tag)
                                  DLlab <- trim(endCapture(save))
                                  indent0 <- indent
                                  opts <- Rd2txt_options()
                                  indent <<- max(opts$minIndent,
                                                 indent + opts$extraIndent)
                                  keepFirstIndent <<- TRUE
                                  linebreak <- identical(opts$descStyle, "linebreak")
                                  suffix <- if (identical(opts$descStyle, "colon")
                                                && !endsWith(DLlab[length(DLlab)], ":")) ": "
                                            else if (!linebreak) " "
                                  putw(strrep(" ", indent0), DLlab, suffix)
                                  if (linebreak) blankLine(0L)
                                  writeContent(block[[2L]], tag)
			  	  blankLine(0L)
                                  indent <<- indent0
                              },
                              "\\value"=,
                              "\\arguments"= {
                                  blankLine()
                                  save <- startCapture()
                                  dropBlank <<- TRUE
                                  writeItemAsCode(tag, block[[1L]])
                                  DLlab <- trim(endCapture(save))
                                  indent0 <- indent
                                  opts <- Rd2txt_options()
                                  indent <<- max(opts$minIndent, indent + opts$extraIndent)
                                  keepFirstIndent <<- TRUE
                                  DLlab <- paste0(DLlab[nzchar(DLlab)], collapse = " ")
                                  putw(frmt(paste0(DLlab, ": "),
                                              justify="right", width=indent))
                                  writeContent(block[[2L]], tag)
			  	  blankLine(0L)
                                  indent <<- indent0
                              },
                              "\\itemize" =,
                              "\\enumerate" = {
                              	  blankLine()
                              	  keepFirstIndent <<- TRUE
                              	  opts <- Rd2txt_options()
                              	  if (blocktag == "\\itemize")
                              	      label <- opts$itemBullet
                              	  else {
                              	      enumItem <<- enumItem + 1L
                              	      label <- opts$enumFormat(enumItem)
                              	  }
                              	  putw(frmt(label, justify="right",
                                            width=indent))
                              })
                       itemskip <- TRUE
                   },
               { # default
                   if (itemskip) {
                       ## The next item must be TEXT, and start with a space.
                       itemskip <- FALSE
                       if (tag == "TEXT") {
                           txt <- psub("^ ", "", as.character(unescape(tabExpand(block))))
                           put(txt)
                           if (!haveBlanks &&
                               blocktag %in% c("\\describe", "\\value", "\\arguments"))
                           dropBlank <<- FALSE  # keep blank line for following text
                       } else writeBlock(block, tag, blocktag) # should not happen
                   } else writeBlock(block, tag, blocktag)
               })
	}
    }

    writeSection <- function(section, tag) {
        if (tag %in% c("\\alias", "\\concept", "\\encoding", "\\keyword"))
            return()
    	save <- c(indent, sectionLevel, keepFirstIndent, dropBlank, wrapping)
    	blankLine(min(sectionLevel, 1L))
    	titlePrefix <- strrep("  ", sectionLevel)
    	opts <- Rd2txt_options()
        indent <<- opts$sectionIndent + opts$sectionExtra*sectionLevel
        sectionLevel <<- sectionLevel + 1
        keepFirstIndent <<- TRUE
        if (tag == "\\section" || tag == "\\subsection") {
            ## section header could have markup
            title <- .Rd_format_title(.Rd_get_text(section[[1L]]))
            putf(titlePrefix, txt_header(title), ":")
            blankLine()
            dropBlank <<- TRUE
            wrapping <<- TRUE
            keepFirstIndent <<- FALSE
    	    writeContent(section[[2L]], tag)
    	} else if (tag %in% c("\\usage", "\\examples")) {
            putf(txt_header(sectionTitles[tag]), ":")
            blankLine()
            dropBlank <<- TRUE
            wrapping <<- FALSE
            keepFirstIndent <<- FALSE
            writeCodeBlock(section, tag)
    	} else {
            putf(txt_header(sectionTitles[tag]), ":")
            blankLine()
            dropBlank <<- TRUE
            wrapping <<- TRUE
            keepFirstIndent <<- FALSE
            writeContent(section, tag)
        }
        blankLine()

        indent <<- save[1L]
        sectionLevel <<- save[2L]
        keepFirstIndent <<- save[3L]
        dropBlank <<- save[4L]
        wrapping <<- save[5L]
    }

    if (is.character(out)) {
        if(out == "") {
            con <- stdout()
        } else {
	    con <- file(out, "wt")
	    on.exit(close(con), add=TRUE)
	}
    } else {
    	con <- out
    	out <- summary(con)$description
    }

    writeItemAsCode <- function(blocktag, block) {
        ## Keep this in rsync with writeItemAsCode() in Rd2HTML.R!
        
        ## Argh.  Quite a few packages put the items in their value
        ## section inside \code.
        for(i in which(RdTags(block) == "\\code"))
            attr(block[[i]], "Rd_tag") <- "Rd"

        s <- as.character.Rd(block)
        s[s %in% c("\\dots", "\\ldots")] <- "..."
        s <- trimws(strsplit(paste(s, collapse = ""), ",", fixed = TRUE)[[1]])
        s <- s[nzchar(s)]
        s <- paste0(s, collapse = ", ")
        putf(s)
    }
        
    Rd <- prepare_Rd(Rd, defines=defines, stages=stages, fragment=fragment, ...)
    Rdfile <- attr(Rd, "Rdfile")
    sections <- RdTags(Rd)
    if (fragment) {
    	if (sections[1L] %in% names(sectionOrder))
    	    for (i in seq_along(sections))
    	    	writeSection(Rd[[i]], sections[i])
    	else
    	    for (i in seq_along(sections))
    	    	writeBlock(Rd[[i]], sections[i], "")
    } else {
	title <- .Rd_format_title(.Rd_get_title(Rd))

	name <- .Rd_topic_for_display(.Rd_get_name(Rd),
                                      .Rd_get_metadata(Rd, "alias"))

	if(nzchar(package)) {
	    left <- name
	    mid <- if(nzchar(package)) paste0("package:", package) else ""
	    right <- "R Documentation"
	    pad <- max(HDR_WIDTH - nchar(left, "w") - nchar(mid, "w") - nchar(right, "w"), 0)
	    pad0 <- pad %/% 2L
	    pad1 <- strrep(" ", pad0)
	    pad2 <- strrep(" ", pad - pad0)
	    putf(paste0(left, pad1, mid, pad2, right, "\n\n"))
	}

	putf(txt_header(title))
	blankLine()

	for (i in seq_along(sections)[-(1:2)])
	    writeSection(Rd[[i]], sections[i])
    }
    blankLine(0L)
    invisible(out)
}
