R/ooxml_block_objects.R

Defines functions plot_instr to_pml.unordered_list print.unordered_list unordered_list to_html.block_list to_pml.block_list to_wml.block_list block_list to_html.fpar to_pml.fpar to_wml.fpar as.data.frame.fpar fortify_fpar fpar to_pml.block_table to_wml.block_table print.block_table block_table table_pptx table_docx to_wml.prop_table prop_table to_wml.table_colwidths to_wml.table_stylenames table_stylenames table_colwidths to_wml.table_width table_width to_wml.table_layout table_layout to_pml.table_conditional_formatting to_wml.table_conditional_formatting table_conditional_formatting to_wml.block_section print.block_section block_section to_wml.block_pour_docx print.block_pour_docx block_pour_docx to_wml.block_toc print.block_toc block_toc to_wml.block_caption to_wml_block_caption_pandoc to_wml_block_caption_officer print.block_caption block_caption

Documented in block_caption block_list block_pour_docx block_section block_table block_toc fpar plot_instr prop_table table_colwidths table_conditional_formatting table_layout table_stylenames table_width unordered_list

# caption ----

#' @export
#' @title Caption block
#' @description Create a representation of a
#' caption that can be used for cross reference.
#' @param label a scalar character representing label to display
#' @param style paragraph style name
#' @param autonum an object generated with function [run_autonum]
#' @examples
#' library(officer)
#'
#' run_num <- run_autonum(seq_id = "tab", pre_label = "tab. ",
#'   bkm = "mtcars_table")
#' caption <- block_caption("mtcars table",
#'   style = "Normal",
#'   autonum = run_num
#' )
#'
#' doc_1 <- read_docx()
#' doc_1 <- body_add(doc_1, "A title", style = "heading 1")
#' doc_1 <- body_add(doc_1, "Hello world!", style = "Normal")
#' doc_1 <- body_add(doc_1, caption)
#' doc_1 <- body_add(doc_1, mtcars, style = "table_template")
#'
#' print(doc_1, target = tempfile(fileext = ".docx"))
#' @family block functions for reporting
block_caption <- function(label, style = NULL, autonum = NULL) {

  if (is.null(style)) {
    style <- "Normal"
  }

  z <- list(
    label = label,
    autonum = autonum,
    style = style
  )

  class(z) <- c("block_caption", "block")
  z
}

#' @export
print.block_caption <- function(x, ...) {
  if (is.null(x$autonum)) {
    auton <- "[autonum off]"
  } else {
    auton <- "[autonum on]"
  }
  cat("caption ", auton, ": ", x$label, "\n", sep = "")
}

to_wml_block_caption_officer <- function(x, add_ns = FALSE){

  open_tag <- wp_ns_no
  if (add_ns) {
    open_tag <- wp_ns_yes
  }

  autonum <- ""
  if (!is.null(x$autonum)) {
    autonum <- to_wml(x$autonum)
  }

  run_str <- sprintf("<w:r><w:t xml:space=\"preserve\">%s</w:t></w:r>", htmlEscapeCopy(x$label))
  run_str <- paste0(autonum, run_str)

  out <- sprintf(
    "%s<w:pPr><w:pStyle w:stlname=\"%s\"/></w:pPr>%s</w:p>",
    open_tag, x$style, run_str
  )

  out
}
to_wml_block_caption_pandoc <- function(x, bookdown_id = NULL){

  if(is.null(x$label)) return("")

  autonum <- ""
  if (!is.null(x$autonum)) {
    autonum <- paste("`", to_wml(x$autonum), "`{=openxml}", sep = "")
  }

  run_str <- paste0(autonum, htmlEscapeCopy(x$label))

  paste0(
    if (!is.null(x$style)) paste0("\n\n::: {custom-style=\"", x$style, "\"}"),
    "\n\n",
    # "<caption>\n\n",
    if (!is.null(bookdown_id)) bookdown_id,
    run_str,
    # "\n\n</caption>",
    if (!is.null(x$style)) paste0("\n:::\n"),
    "\n\n"
  )
}

#' @export
to_wml.block_caption <- function(x, add_ns = FALSE, knitting = FALSE, ...) {
  if(knitting)
    to_wml_block_caption_pandoc(x, bookdown_id = list(...)$bookdown_id)
  else
    to_wml_block_caption_officer(x, add_ns = add_ns)
}


# toc ----

#' @export
#' @title Table of content for 'Word'
#' @description Create a representation of a table
#' of content for Word documents.
#' @param level max title level of the table
#' @param style optional. If not NULL, its value is used as style in the
#' document that will be used to build entries of the TOC.
#' @param seq_id optional. If not NULL, its value is used as sequence
#' identifier in the document that will be used to build entries of the
#' TOC. See also [run_autonum()] to specify a sequence identifier.
#' @param separator optional. Some configurations need "," (i.e. from Canada) separator instead of ";"
#' @examples
#' block_toc(level = 2)
#' block_toc(style = "Table Caption")
#' @family block functions for reporting
block_toc <- function(level = 3, style = NULL, seq_id = NULL, separator = ";") {
  z <- list(
    level = level, style = style, seq_id = seq_id, separator = separator
  )
  class(z) <- c("block_toc", "block")

  z
}

#' @export
print.block_toc <- function(x, ...) {
  if (is.null(x$style) && is.null(x$seq_id)) {
    cat("TOC - max level: ", x$level, "\n", sep = "")
  } else if (!is.null(x$style)) {
    cat("TOC for style: ", x$style, "\n", sep = "")
  } else if (!is.null(x$seq_id)) {
    cat("TOC for seq identifier: ", x$seq_id, "\n", sep = "")
  }
}

#' @export
to_wml.block_toc <- function(x, add_ns = FALSE, ...) {

  open_tag <- wp_ns_no
  if (add_ns) {
    open_tag <- wp_ns_yes
  }


  if(is.null(x$style) && is.null(x$seq_id)) {
    out <- paste0(
      open_tag,
      "<w:pPr/>",
      to_wml(
        run_word_field(
          field = sprintf(
            "TOC \\o \"1-%.0f\" \\h \\z \\u",
            x$level
          )
        )
      ),
      "</w:p>"
    )
  } else if(!is.null(x$style)) {
    out <- paste0(
      open_tag,
      "<w:pPr/>",
      to_wml(
        run_word_field(
          field = sprintf(
            "TOC \\h \\z \\t \"%s%s1\"",
            x$style, x$separator
          )
        )
      ),
      "</w:p>"
    )
  } else if(!is.null(x$seq_id)) {
    out <- paste0(
      open_tag,
      "<w:pPr/>",
      to_wml(
        run_word_field(
          field = sprintf("TOC \\h \\z \\c \"%s\"", x$seq_id)
        )
      ),
      "</w:p>"
    )
  }



  out
}

# pour_docx ----

#' @export
#' @title External Word document placeholder
#' @description Pour the content of a docx file in the resulting docx
#' from an 'R Markdown' document.
#' @param file external docx file path
#' @examples
#' library(officer)
#' docx <- tempfile(fileext = ".docx")
#' doc <- read_docx()
#' doc <- body_add(doc, iris[1:20,], style = "table_template")
#' print(doc, target = docx)
#'
#' target <- tempfile(fileext = ".docx")
#' doc_1 <- read_docx()
#' doc_1 <- body_add(doc_1, block_pour_docx(docx))
#' print(doc_1, target = target)
#' @family block functions for reporting
block_pour_docx <- function(file){
  if(!file.exists(file)){
    stop("file {", file, "} does not exist.", call. = FALSE)
  }
  if(!grepl("\\.docx$", file, ignore.case = TRUE)){
    stop("file {", file, "} is not a docx file.", call. = FALSE)
  }
  if(grepl("&", file, ignore.case = TRUE)){
    stop("file path {", file, "} contains '&', please rename your file.", call. = FALSE)
  }
  if(grepl(" ", basename(file), ignore.case = TRUE)){
    stop("file path {", basename(file), "} contains ' ', please rename your file.", call. = FALSE)
  }

  z <- list(file = file)
  class(z) <- c("block_pour_docx", "block")
  z
}

#' @export
print.block_pour_docx <- function(x, ...) {
  cat("Pour docx: ", x$file, "\n", sep = "")
}

#' @export
to_wml.block_pour_docx <- function(x, add_ns = FALSE, ...) {
  ns_str <- ""
  if (add_ns) {
    ns_str <- "xmlns:w=\"http://schemas.openxmlformats.org/wordprocessingml/2006/main\" xmlns:r=\"http://schemas.openxmlformats.org/officeDocument/2006/relationships\" "
  }
  out <- paste0("<w:altChunk ", ns_str,
                    "r:id=\"", x$file, "\"/>")
  out
}



# section ----

#' @export
#' @title Section for 'Word'
#' @description Create a representation of a section.
#'
#' A section affects preceding paragraphs or tables; i.e.
#' a section starts at the end of the previous section (or the beginning of
#' the document if no preceding section exists), and stops where the
#' section is declared.
#'
#' When a new landscape section is needed, it is recommended to add a block_section
#' with `type = "continuous"`, to add the content to be appened in the new section
#' and finally to add a block_section with `page_size = page_size(orient = "landscape")`.
#' @param property section properties defined with function [prop_section]
#' @examples
#' ps <- prop_section(
#'   page_size = page_size(orient = "landscape"),
#'   page_margins = page_mar(top = 2),
#'   type = "continuous"
#' )
#' block_section(ps)
#' @family block functions for reporting
block_section <- function(property) {
  z <- list(
    property = property
  )
  class(z) <- c("block_section", "block")

  z
}

#' @export
print.block_section <- function(x, ...) {
  cat("----- end of section: ", "\n", sep = "")
}

#' @export
to_wml.block_section <- function(x, add_ns = FALSE, ...) {
  open_tag <- wp_ns_no
  if (add_ns) {
    open_tag <- wp_ns_yes
  }

  out <- paste0(open_tag,
    "<w:pPr>",
    to_wml(x$property),
    "</w:pPr></w:p>"
  )

  out
}


# table properties ----


#' @export
#' @title Table conditional formatting
#' @description Tables can be conditionally formatted based on few properties as
#' whether the content is in the first row, last row, first column, or last
#' column, or whether the rows or columns are to be banded.
#' @param first_row,last_row apply or remove formatting from the first or last row in the table.
#' @param first_column,last_column apply or remove formatting from the first or last column in the table.
#' @param no_hband,no_vband don't display odd and even rows or columns with
#' alternating shading for ease of reading.
#' @note
#' You must define a format for first_row, first_column and other properties
#' if you need to use them. The format is defined in a docx template.
#' @examples
#' table_conditional_formatting(first_row = TRUE, first_column = TRUE)
#' @family functions for table definition
table_conditional_formatting <- function(
  first_row = TRUE, first_column = FALSE,
  last_row = FALSE, last_column = FALSE,
  no_hband = FALSE, no_vband = TRUE){

  z <- list(first_row = first_row, first_column = first_column,
            last_row = last_row, last_column = last_column,
            no_hband = no_hband, no_vband = no_vband)
  class(z) <- c("table_conditional_formatting")
  z
}

#' @export
to_wml.table_conditional_formatting <- function(x, add_ns = FALSE, ...) {
  paste0("<w:tblLook w:firstRow=\"", as.integer(x$first_row),
         "\" w:lastRow=\"", as.integer(x$last_row),
         "\" w:firstColumn=\"", as.integer(x$first_column),
         "\" w:lastColumn=\"", as.integer(x$last_column),
         "\" w:noHBand=\"", as.integer(x$no_hband),
         "\" w:noVBand=\"", as.integer(x$no_vband), "\"/>")
}

#' @export
to_pml.table_conditional_formatting <- function(x, add_ns = FALSE, ...){
  expr_ <- paste0(" firstRow=\"%.0f\" lastRow=\"%.0f\"",
         " firstColumn=\"%.0f\" lastColumn=\"%.0f\"",
         " bandRow=\"%.0f\" bandCol=\"%.0f\""
         )
  sprintf(expr_,
          x$first_row, x$last_row,
          x$first_column, x$last_column,
          !x$no_hband, !x$no_vband)
}

table_layout_types <- c("autofit", "fixed")


#' @export
#' @title Algorithm for table layout
#' @description When a table is displayed in a document, it can
#' either be displayed using a fixed width or autofit layout algorithm:
#'
#' * fixed: uses fixed widths for columns. The width of the table is not
#' changed regardless of the contents of the cells.
#' * autofit: uses the contents of each cell and the table width to
#' determine the final column widths.
#' @param type 'autofit' or 'fixed' algorithm. Default to 'autofit'.
#' @family functions for table definition
table_layout <- function(type = "autofit"){

  if(!type %in% table_layout_types){
    stop("type must be one of ", paste(table_layout_types, collapse = ", "), ".")
  }

  z <- list(type = type)
  class(z) <- "table_layout"
  z
}

#' @export
to_wml.table_layout <- function(x, add_ns = FALSE, ...) {
  sprintf("<w:tblLayout w:type=\"%s\"/>", x$type)
}

table_layout_width_units <- c("in", "pct")

#' @export
#' @title Preferred width for a table
#' @description Define the preferred width for a table.
#' @section Word:
#' All widths in a table are considered preferred because widths of
#' columns can conflict and the table layout rules can require a
#' preference to be overridden.
#' @param width value of the preferred width of the table.
#' @param unit unit of the width. Possible values are 'in' (inches) and 'pct' (percent)
#' @family functions for table definition
table_width <- function(width = 1, unit = "pct"){
  if(!unit %in% table_layout_width_units){
    stop("unit must be one of ", paste(table_layout_width_units, collapse = ", "), ".")
  }

  z <- list(width = width, unit = unit)
  class(z) <- "table_width"
  z

}
#' @export
to_wml.table_width <- function(x, add_ns = FALSE, ...) {
  if(x$unit %in% "pct"){
    tbl_width <- sprintf("<w:tblW w:type=\"pct\" w:w=\"%.0f\"/>",
                         x$width * 5000)
  } else {
    tbl_width <- sprintf("<w:tblW w:type=\"dxa\" w:w=\"%.0f\"/>",
                         x$width * 1440)
  }
  tbl_width
}

#' @export
#' @title Column widths of a table
#' @description The function defines the size of each column of a table.
#' @param widths Column widths expressed in inches.
#' @family functions for table definition
table_colwidths <- function(widths = NULL){
  z <- list(widths = widths)
  class(z) <- "table_colwidths"
  z
}

#' @export
#' @title Paragraph styles for columns
#' @description The function defines the paragraph styles for columns.
#' @param stylenames a named character vector, names are column names, values are
#' paragraph styles associated with each column. If a column is not
#' specified, default value 'Normal' is used.
#' Another form is as a named list, the list names are the styles
#' and the contents are column names to be formatted with the
#' corresponding style.
#' @family functions for table definition
#' @examples
#' library(officer)
#'
#' stylenames <- c(
#'   vs = "centered", am = "centered",
#'   gear = "centered", carb = "centered"
#' )
#'
#' doc_1 <- read_docx()
#' doc_1 <- body_add_table(doc_1,
#'   value = mtcars, style = "table_template",
#'   stylenames = table_stylenames(stylenames = stylenames)
#' )
#'
#' print(doc_1, target = tempfile(fileext = ".docx"))
#'
#'
#' stylenames <- list(
#'   "centered" = c("vs", "am", "gear", "carb")
#' )
#'
#' doc_2 <- read_docx()
#' doc_2 <- body_add_table(doc_2,
#'   value = mtcars, style = "table_template",
#'   stylenames = table_stylenames(stylenames = stylenames)
#' )
#'
#' print(doc_2, target = tempfile(fileext = ".docx"))
table_stylenames <- function(stylenames = list()){

  if( length(stylenames) > 0 && is.null(attr(stylenames, "names")) ){
    stop("stylenames should have names")
  }

  if( length(stylenames) > 0 && is.list(stylenames) ){
    .l <- vapply(stylenames, length, FUN.VALUE = 0L)
    zz <- inverse.rle(
      structure(list(
        lengths = .l,
        values = names(stylenames)),
        class = "rle")
      )
    names(zz) <- as.character(unlist(stylenames))
    stylenames <- as.list(zz)
  } else if(is.character(stylenames)){
    stylenames <- as.list(stylenames)
  }

  z <- list(stylenames = stylenames)
  class(z) <- "table_stylenames"
  z
}

#' @export
#' @importFrom utils modifyList
to_wml.table_stylenames <- function(x, add_ns = FALSE, dat, ...) {

  stylenames <- rep("Normal", ncol(dat))
  names(stylenames) <- colnames(dat)
  stylenames <- as.list(stylenames)
  # restrict to only existing cols
  x$stylenames <- x$stylenames[names(x$stylenames) %in% colnames(dat)]
  stylenames <- modifyList(stylenames, val = x$stylenames)
  stylenames <- lapply(stylenames, function(x){
    sprintf("<w:pStyle w:stlname=\"%s\"/>", x)
  })
  stylenames
}

#' @export
to_wml.table_colwidths <- function(x, add_ns = FALSE, ...) {
  if(length(x$widths) < 1) return("")
  grid_col_str <- sprintf("<w:gridCol w:w=\"%.0f\"/>", x$widths * 1440)
  grid_col_str <- paste(grid_col_str, collapse = "")
  paste0("<w:tblGrid>", grid_col_str, "</w:tblGrid>")
}

#' @export
#' @title Table properties
#' @description Define table properties such as fixed or autofit layout,
#' table width in the document, eventually column widths.
#' @param style table style to be used to format table
#' @param layout layout defined by [table_layout()],
#' @param width table width in the document defined by [table_width()]
#' @param stylenames columns styles defined by [table_stylenames()]
#' @param colwidths column widths defined by [table_colwidths()]
#' @param align table alignment (one of left, center or right)
#' @param tcf conditional formatting settings defined by [table_conditional_formatting()]
#' @param word_title alternative text for Word table (used as title of the table)
#' @param word_description alternative text for Word table (used as description of the table)
#' @examples
#' prop_table()
#' to_wml(prop_table())
#' @family functions for table definition
prop_table <- function(style = NA_character_, layout = table_layout(),
                       width = table_width(),
                       stylenames = table_stylenames(),
                       colwidths = table_colwidths(),
                       tcf = table_conditional_formatting(),
                       align = "center",
                       word_title = NULL,
                       word_description = NULL){


  z <- list(
    style = style,
    layout = layout,
    width = width,
    colsizes = colwidths,
    stylenames = stylenames,
    tcf = tcf, align = align,
    word_title = word_title,
    word_description = word_description
  )
  class(z) <- c("prop_table")
  z
}

#' @export
to_wml.prop_table <- function(x, add_ns = FALSE, base_document = NULL, ...) {

  style <- NA_character_
  if(!is.null(x$style) && !is.na(x$style)){
    if (is.null(x$style) && !is.null(base_document$default_styles$table)) {
      style <- base_document$default_styles$table
    } else {
      style <- x$style
    }
  }

  tbl_layout <- to_wml(x$layout, add_ns= add_ns)

  width <- ""
  if(!is.null(x$width) && "autofit" %in% x$layout$type)
    width <- to_wml(x$width, add_ns= add_ns)

  colwidths <- to_wml(x$colsizes, add_ns= add_ns)
  tcf <- to_wml(x$tcf, add_ns= add_ns)
  paste0("<w:tblPr>",
         if(!is.null(x$word_title)) paste0("<w:tblCaption w:val=\"", htmlEscapeCopy(x$word_title), "\"/>"),
         if(!is.null(x$word_description)) paste0("<w:tblDescription w:val=\"", htmlEscapeCopy(x$word_description), "\"/>"),
         if(!is.na(style)) paste0("<w:tblStyle w:stlname=\"", style, "\"/>"),
         tbl_layout,
         sprintf( "<w:jc w:val=\"%s\"/>", x$align ),
         width, tcf,
         "</w:tblPr>",
         if(x$layout$type %in% "fixed") colwidths
         )

}


# table ----
table_docx <- function(x, header, style_id,
                       properties, alignment = NULL, add_ns = FALSE,
                       base_document = base_document) {
  open_tag <- tbl_ns_no
  if (add_ns) {
    open_tag <- tbl_ns_yes
  }

  str <- paste0(
    open_tag,
    to_wml(properties, add_ns = add_ns, base_document = base_document)
  )

  stylenames <- to_wml(properties$stylenames, base_document = base_document, dat = x)
  stylenames <- unlist(stylenames)
  stylenames <- as.character(stylenames)

  if(is.null(alignment)){
    alignment <- rep("", ncol(x))
  } else{
    alignment <- match.arg(alignment, c("left", "right", "center"), several.ok = TRUE )
    if(length(alignment) < ncol(x)){
      alignment <- rep(alignment, length.out = ncol(x) )
    }
    alignment <- sprintf("<w:jc w:val=\"%s\"/>", alignment)
  }

  header_str <- character(length = 0L)
  if (header) {

    header_str <- paste0(
      "<w:tr><w:trPr><w:tblHeader/></w:trPr>",
      paste0("<w:tc><w:p>",
             sprintf("<w:pPr>%s%s</w:pPr>", stylenames, alignment),
             "<w:r><w:t>",
        htmlEscapeCopy(enc2utf8(colnames(x))),
        "</w:t></w:r></w:p></w:tc>",
        collapse = ""
      ),
      "</w:tr>"
    )
  }
  as_tc <- function(x, align, stylenames) {
    paste0(
      "<w:tc><w:p>",
      sprintf("<w:pPr>%s%s</w:pPr>", stylenames, align),
      "<w:r><w:t>",
      htmlEscapeCopy(enc2utf8(x)),
      "</w:t></w:r></w:p></w:tc>"
    )
  }
  z <- mapply(as_tc, x, alignment, stylenames, SIMPLIFY = FALSE)
  z <- do.call(paste0, z)
  z <- paste0("<w:tr>", z, "</w:tr>", collapse = "")

  paste0(str, header_str, z, "</w:tbl>")
}

table_pptx <- function(x, style_id, col_width, row_height,
                       tcf, header = TRUE, alignment = NULL ){
  str <- paste0("<a:tbl>",
                sprintf("<a:tblPr %s>", to_pml(tcf)),
                sprintf("<a:tableStyleId>%s</a:tableStyleId>", style_id),
                "</a:tblPr>",
                "<a:tblGrid>",
                paste0(sprintf("<a:gridCol w=\"%.0f\"/>",
                               rep(col_width, length(x))),
                       collapse = ""),
                "</a:tblGrid>")

  as_tc <- function(x, align) {
    paste0("<a:tc><a:txBody><a:bodyPr/><a:lstStyle/><a:p>",
           "<a:pPr algn=\"", align, "\"/>",
           "<a:r><a:t>",
           htmlEscapeCopy(enc2utf8(x)),
           "</a:t></a:r></a:p></a:txBody></a:tc>"
    )
  }

  if(is.null(alignment)){
    alignment <- rep("r", ncol(x))
  } else{
    alignment <- match.arg(alignment, c("l", "r", "ctr"), several.ok = TRUE )
  }

  header_str <- character(length = 0L)
  if( header ){
    header_str  <- paste0(
      sprintf("<a:tr h=\"%.0f\">", row_height),
      paste0(as_tc(colnames(x), align = alignment), collapse = ""),
      "</a:tr>"
    )
  }

  z <- mapply(as_tc, x, alignment, SIMPLIFY = FALSE)
  z <- do.call(paste0, z)
  z <- paste0(sprintf("<a:tr h=\"%.0f\">", row_height), z, "</a:tr>", collapse = "")

  z <- paste0(str, header_str, z, "</a:tbl>")
  z <- paste0(
    "<a:graphic>",
    "<a:graphicData uri=\"http://schemas.openxmlformats.org/drawingml/2006/table\">",
    z, "</a:graphicData>", "</a:graphic>")
}


#' @export
#' @title Table block
#' @description Create a representation of a table
#' @param x a data.frame to add as a table
#' @param header display header if TRUE
#' @param properties table properties, see [prop_table()].
#' Table properties are not handled identically between Word and PowerPoint
#' output format. They are fully supported with Word but for PowerPoint (which
#' does not handle as many things as Word for tables), only conditional
#' formatting properties are supported.
#' @param alignment alignment for each columns, 'l' for left, 'r' for right
#' and 'c' for center. Default to NULL.
#' @examples
#' block_table(x = head(iris))
#'
#' block_table(x = mtcars, header = TRUE,
#'   properties = prop_table(
#'     tcf = table_conditional_formatting(
#'       first_row = TRUE, first_column = TRUE)
#'   ))
#' @family block functions for reporting
#' @seealso [prop_table()]
block_table <- function(x, header = TRUE, properties = prop_table(), alignment = NULL) {

  stopifnot(is.data.frame(x))
  if(inherits(x, "tbl_df"))
    x <- as.data.frame(
      x, check.names = FALSE, stringsAsFactors = FALSE )

  z <- list(
    x = x,
    header = header,
    properties = properties,
    alignment = alignment
  )
  class(z) <- c("block_table", "block")

  z
}

#' @export
#' @importFrom utils str
print.block_table <- function(x, ...) {
  str(x$x)
}

#' @export
to_wml.block_table <- function(x, add_ns = FALSE, base_document = NULL, ...) {

  value <- characterise_df(x$x)

  out <- table_docx(
    x = value, header = x$header,
    properties = x$properties,
    alignment = x$alignment,
    base_document = base_document,
    add_ns = add_ns
  )

  out
}

#' @export
to_pml.block_table <- function(x, add_ns = FALSE,
                               left = 0, top = 0, width = 3, height = 3,
                               bg = "transparent", rot = 0, label = "", ph = "<p:ph/>", ...){

  if( !is.null(bg) && !is.color( bg ) )
    stop("bg must be a valid color.", call. = FALSE )

  bg_str <- solid_fill_pml(bg)

  xfrm_str <- p_xfrm_str(left = left, top = top, width = width, height = height, rot = rot)
  if( is.null(ph) || is.na(ph)){
    ph = "<p:ph/>"
  }
  value <- characterise_df(x$x)
  value_str <- table_pptx(value, style_id = x$properties$style,
                          alignment = x$alignment,
                        col_width = as.integer((width/ncol(x$x))*914400),
                        row_height = as.integer((height/nrow(x$x))*914400),
                        tcf = x$properties$tcf,
                        header = x$header )

  id <- uuid_generate()
  str <- paste0("<p:graphicFrame xmlns:a=\"http://schemas.openxmlformats.org/drawingml/2006/main\" xmlns:r=\"http://schemas.openxmlformats.org/officeDocument/2006/relationships\" xmlns:p=\"http://schemas.openxmlformats.org/presentationml/2006/main\">",
                "<p:nvGraphicFramePr>",
                sprintf("<p:cNvPr id=\"%s\" name=\"%s\"/>", id, label),
                "<p:cNvGraphicFramePr><a:graphicFrameLocks noGrp=\"1\"/></p:cNvGraphicFramePr>",
                sprintf("<p:nvPr>%s</p:nvPr>", ph),
                "</p:nvGraphicFramePr>",
                xfrm_str,
                bg_str,
                value_str,
                "</p:graphicFrame>")
  str
}

# fpar ----

#' @export
#' @title Formatted paragraph
#' @description Create a paragraph representation by concatenating
#' formatted text or images. The result can be inserted in a Word document
#' or a PowerPoint presentation and can also be inserted in a [block_list()]
#' call.
#'
#' All its arguments will be concatenated to create a paragraph where chunks of
#' text and images are associated with formatting properties.
#'
#' \code{fpar} supports [ftext()], [external_img()], \code{run_*} functions
#' (i.e. [run_autonum()], [run_word_field()]) when output is Word, and simple strings.
#'
#' Default text and paragraph formatting properties can also be modified
#' with function `update()`.
#'
#' @param ... cot objects ([ftext()], [external_img()])
#' @param fp_p paragraph formatting properties, see [fp_par()]
#' @param fp_t default text formatting properties. This is used as
#' text formatting properties when simple text is provided as argument,
#' see [fp_text()].
#' @param values a list of cot objects. If provided, argument \code{...} will be ignored.
#' @param object fpar object
#' @examples
#' fpar(ftext("hello", shortcuts$fp_bold()))
#'
#' # mix text and image -----
#' img.file <- file.path( R.home("doc"), "html", "logo.jpg" )
#'
#' bold_face <- shortcuts$fp_bold(font.size = 12)
#' bold_redface <- update(bold_face, color = "red")
#' fpar_1 <- fpar(
#'   "Hello World, ",
#'   ftext("how ", prop = bold_redface ),
#'   external_img(src = img.file, height = 1.06/2, width = 1.39/2),
#'   ftext(" you?", prop = bold_face ) )
#' fpar_1
#'
#' img_in_par <- fpar(
#'   external_img(src = img.file, height = 1.06/2, width = 1.39/2),
#'   fp_p = fp_par(text.align = "center") )
#' @family block functions for reporting
#' @seealso [block_list()], [body_add_fpar()], [ph_with()]
fpar <- function( ..., fp_p = fp_par(), fp_t = fp_text_lite(), values = NULL) {
  out <- list()

  if( is.null(values)){
    values <- list(...)
  }

  out$chunks <- values
  out$fp_p <- fp_p
  out$fp_t <- fp_t
  class(out) <- c("fpar", "block")
  out
}

#' @export
#' @rdname fpar
#' @importFrom stats update
update.fpar <- function (object, fp_p = NULL, fp_t = NULL, ...){

  if(!is.null(fp_p)){
    object$fp_p <- fp_p
  }
  if(!is.null(fp_t)){
    object$fp_t <- fp_t
  }

  object
}


fortify_fpar <- function(x){
  lapply(x$chunks, function(chk) {
    if( !inherits(chk, c("cot", "run")) ){
      chk <- ftext(text = format(chk), prop = x$fp_t )
    }
    chk
  })
}


#' @export
as.data.frame.fpar <- function( x, ...){
  chks <- fortify_fpar(x)
  chks <- chks[sapply(chks, function(x) inherits(x, "ftext"))]
  chks <- mapply(function(x){
    data.frame(value = x$value, size = x$pr$font.size,
               bold = x$pr$bold, italic = x$pr$italic,
               font.family = x$pr$font.family, stringsAsFactors = FALSE )
  }, chks, SIMPLIFY = FALSE)
  rbind_match_columns(chks)
}


#' @export
to_wml.fpar <- function(x, add_ns = FALSE, style_id = NULL, ...) {

  open_tag <- wp_ns_no
  if (add_ns) {
    open_tag <- wp_ns_yes
  }
  if(is.null(style_id)){
    par_style <- ppr_wml(x$fp_p)
  } else par_style <- paste0(
    "<w:pPr><w:pStyle w:val=\"", style_id, "\"/></w:pPr>")

  chks <- fortify_fpar(x)
  z <- lapply(chks, to_wml)
  z$collapse <- ""
  z <- do.call(paste0, z)
  paste0(open_tag, par_style, z, "</w:p>")
}

#' @export
to_pml.fpar <- function(x, add_ns = FALSE, ...) {

  open_tag <- ap_ns_no
  if (add_ns) {
    open_tag <- ap_ns_yes
  }

  par_style <- ppr_pml(x$fp_p)
  chks <- fortify_fpar(x)
  z <- lapply(chks, to_pml)
  z$collapse <- ""
  z <- do.call(paste0, z)
  paste0(open_tag, par_style, z, "</a:p>")
}


#' @export
to_html.fpar <- function(x, add_ns = FALSE, ...) {
  par_style <- ppr_css(x$fp_p)
  chks <- fortify_fpar(x)
  z <- lapply(chks, to_html)
  z$collapse <- ""
  z <- do.call(paste0, z)
  sprintf("<p style=\"%s\">%s</p>", par_style, z)
}

# block_list -----

#' @export
#' @title List of blocks
#' @description A list of blocks can be used to gather
#' several blocks (paragraphs, tables, ...) into a single
#' object. The result can be added into a Word document or a
#' PowerPoint presentation.
#' @param ... a list of blocks. When output is only for
#' Word, objects of class [external_img()] can
#' also be used in fpar construction to mix text and images
#' in a single paragraph. Supported objects are:
#' [block_caption()], [block_pour_docx()], [block_section()],
#' [block_table()], [block_toc()], [fpar()], [plot_instr()].
#' @examples
#' # block list ------
#'
#' img.file <- file.path( R.home("doc"), "html", "logo.jpg" )
#' fpt_blue_bold <- fp_text(color = "#006699", bold = TRUE)
#' fpt_red_italic <- fp_text(color = "#C32900", italic = TRUE)
#'
#'
#' ## This can be only be used in a MS word output as pptx does
#' ## not support paragraphs made of text and images.
#' ## (actually it can be used but image will not appear in the
#' ## pptx output)
#' value <- block_list(
#'   fpar(ftext("hello world", fpt_blue_bold)),
#'   fpar(ftext("hello", fpt_blue_bold), " ",
#'        ftext("world", fpt_red_italic)),
#'   fpar(
#'     ftext("hello world", fpt_red_italic),
#'           external_img(
#'             src = img.file, height = 1.06, width = 1.39)))
#' value
#'
#' doc <- read_docx()
#' doc <- body_add(doc, value)
#' print(doc, target = tempfile(fileext = ".docx"))
#'
#'
#' value <- block_list(
#'   fpar(ftext("hello world", fpt_blue_bold)),
#'   fpar(ftext("hello", fpt_blue_bold), " ",
#'        ftext("world", fpt_red_italic)),
#'   fpar(
#'     ftext("blah blah blah", fpt_red_italic)))
#' value
#'
#' doc <- read_pptx()
#' doc <- add_slide(doc)
#' doc <- ph_with(doc, value, location = ph_location_type(type = "body"))
#' print(doc, target = tempfile(fileext = ".pptx"))
#' @seealso [ph_with()], [body_add_blocks()], [fpar()]
#' @family block functions for reporting
block_list <- function(...){
  x <- list(...)
  z <- list()
  for(i in x){
    if(inherits(i, "block")) {
      z <- append(z, list(i))
    } else if(inherits(i, "flextable")){
      z <- append(z, list(i))
    } else if(is.character(i)){
      z <- append(z, lapply(i, fpar))
    }
  }
  class(z) <- c("block_list", "block")
  z
}

#' @export
to_wml.block_list <- function(x, add_ns = FALSE, ...) {
  out <- character(length(x))
  for(i in seq_along(x) ){
    out[i] <- to_wml(x[[i]], add_ns = add_ns)
  }

  paste0(out, collapse = "")
}

#' @export
to_pml.block_list <- function(x, add_ns = FALSE, ...) {
  pars <- sapply(x, to_pml)
  pars <- paste0(pars, collapse = "")
  pars
}

#' @export
to_html.block_list <- function(x, add_ns = FALSE, ...) {
  str <- vapply(x, to_html, NA_character_)
  paste0(str, collapse = "")
}

# unordered list ----
#' @export
#' @title Unordered list
#' @description unordered list of text for PowerPoint
#' presentations. Each text is associated with
#' a hierarchy level.
#' @param str_list list of strings to be included in the object
#' @param level_list list of levels for hierarchy structure. Use
#' 0 for 'no bullet', 1 for level 1, 2 for level 2 and so on.
#' @param style text style, a \code{fp_text} object list or a
#' single \code{fp_text} objects. Use \code{fp_text(font.size = 0, ...)} to
#' inherit from default sizes of the presentation.
#' @examples
#' unordered_list(
#' level_list = c(1, 2, 2, 3, 3, 1),
#' str_list = c("Level1", "Level2", "Level2", "Level3", "Level3", "Level1"),
#' style = fp_text(color = "red", font.size = 0) )
#' unordered_list(
#' level_list = c(1, 2, 1),
#' str_list = c("Level1", "Level2", "Level1"),
#' style = list(
#'   fp_text(color = "red", font.size = 0),
#'   fp_text(color = "pink", font.size = 0),
#'   fp_text(color = "orange", font.size = 0)
#'   ))
#' @seealso \code{\link{ph_with}}
#' @family block functions for reporting
unordered_list <- function(str_list = character(0), level_list = integer(0), style = NULL){
  stopifnot(is.character(str_list))
  stopifnot(is.numeric(level_list))

  if (length(str_list) != length(level_list) & length(str_list) > 0) {
    stop("str_list and level_list have different lenghts.")
  }

  if( !is.null(style)){
    if( inherits(style, "fp_text") )
      style <- lapply(seq_len(length(str_list)), function(x) style )
  }
  x <- list(
    str = str_list,
    lvl = level_list,
    style = style
  )
  class(x) <- "unordered_list"
  x
}
#' @export
#' @noRd
print.unordered_list <- function(x, ...){
  print(data.frame(str = x$str,
                   lvl = x$lvl,
                   stringsAsFactors = FALSE))
  invisible()
}

#' @export
to_pml.unordered_list <- function(x, add_ns = FALSE, ...) {

  open_tag <- ap_ns_no
  if (add_ns) {
    open_tag <- ap_ns_yes
  }
  if( !is.null(x$style)){
    style_str <- sapply(x$style, format, type = "pml")
    style_str <- rep_len(style_str, length.out = length(x$str))
  } else style_str <- rep("<a:rPr/>", length(x$str))
  tmpl <- "%s<a:pPr%s>%s</a:pPr><a:r>%s<a:t>%s</a:t></a:r></a:p>"
  lvl <- sprintf(" lvl=\"%.0f\"", x$lvl - 1)
  lvl <- ifelse(x$lvl > 1, lvl, "")
  bu_none <- ifelse(x$lvl < 1, "<a:buNone/>", "")
  p <- sprintf(tmpl, open_tag, lvl, bu_none, style_str, htmlEscapeCopy(x$str) )
  p <- paste(p, collapse = "")
  p
}

# plot_instr -----
#' @title Wrap plot instructions for png plotting in Powerpoint or Word
#' @description A simple wrapper to capture
#' plot instructions that will be executed and copied in a document. It produces
#' an object of class 'plot_instr' with a corresponding method [ph_with()] and
#' [body_add_plot()].
#'
#' The function enable usage of any R plot with argument `code`. Wrap your code
#' between curly bracket if more than a single expression.
#'
#' @param code plotting instructions
#' @examples
#' # plot_instr demo ----
#'
#' anyplot <- plot_instr(code = {
#'   barplot(1:5, col = 2:6)
#'   })
#'
#' doc <- read_docx()
#' doc <- body_add(doc, anyplot, width = 5, height = 4)
#' print(doc, target = tempfile(fileext = ".docx"))
#'
#'
#' doc <- read_pptx()
#' doc <- add_slide(doc)
#' doc <- ph_with(
#'   doc, anyplot,
#'   location = ph_location_fullsize(),
#'   bg = "#00000066", pointsize = 12)
#' print(doc, target = tempfile(fileext = ".pptx"))
#' @export
#' @import graphics
#' @seealso [ph_with()], [body_add_plot()]
#' @family block functions for reporting
plot_instr <- function(code) {
  out <- list()
  out$code <- substitute(code)
  class(out) <- "plot_instr"
  return(out)
}

Try the officer package in your browser

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

officer documentation built on Oct. 10, 2024, 1:06 a.m.