Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

function for making new blog posts #171

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions DESCRIPTION
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As those two deps are only required for the new blog post function, and this function is aimed to be used mostly interactively, I wonder if we could rely on rlang::check_installed()

By using if(rlang::check_installed("whoami")), user will be asked to install if missing when used interactively.

This would allow to make them suggested dependencies.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For fs, we probably could start using it in this package.

Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ Depends:
R (>= 3.6)
Imports:
cli,
fs,
jsonlite,
later,
processx,
Expand All @@ -26,6 +27,7 @@ Imports:
rstudioapi,
tools,
utils,
whoami,
yaml
Suggests:
curl,
Expand Down
1 change: 1 addition & 0 deletions NAMESPACE
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# Generated by roxygen2: do not edit by hand

export(is_using_quarto)
export(new_blog_post)
export(quarto_add_extension)
export(quarto_binary_sitrep)
export(quarto_create_project)
Expand Down
2 changes: 2 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# quarto (development version)

- Added a `new_blog_post()` function (#22).
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
- Added a `new_blog_post()` function (#22).
- Added a `new_blog_post()` function (thanks, @topepo , #22).


- Approval check in `quarto_add_extension()` and `quarto_use_template()` now works correctly (thanks, @eveyp, #172).

# quarto 1.4
Expand Down
82 changes: 82 additions & 0 deletions R/new-blog-post.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
#' Create a new blog post
#'
#' Creates (and potentially opens) the `index.qmd` file for a new blog post.
#'
#' @param title A character string for the title of the post. It is converted
#' to title case via [tools::toTitleCase()].
#' @param dest A character string (or NULL) for the path within `posts`. By
#' default, the title is adapted as the directory name.
#' @param open A logical: have the default editor open a window to edit the
#' `index.qmd` file?
#' @param call A call object for reporting errors.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My understanding is that the prefered way is to use

⁠@inheritParams rlang::args_error_context

From https://rlang.r-lib.org/reference/args_error_context.html and usage at several tidyverse packages

#' @param ... A named list of values to be added to the yaml header, such as
#' `description`, `author`, `categories`, etc.
#' @return The path to the index file.
#' @export
#' @examples
#' \dontrun{
cderv marked this conversation as resolved.
Show resolved Hide resolved
#' \donttest{
#' new_blog_post("making quarto blog posts", categories = c("R"))
#'
#' }
#' }
#'
new_blog_post <- function(title, dest = NULL, open = rlang::is_interactive(),
call = rlang::current_env(), ...) {

if (is.null(dest)) {
# Scrub title to make directory name
dest <- gsub("[[:space:]]", "-", tolower(title))
topepo marked this conversation as resolved.
Show resolved Hide resolved
}
dest_path <- make_post_dir(dest, call)
post_yaml <- make_post_yaml(title, ...)
qmd_path <- write_post_yaml(post_yaml, dest_path, call)
if (open) {
utils::file.edit(qmd_path)
}
invisible(qmd_path)
}

make_post_dir <- function(dest, call) {
working <- fs::path_wd()

post_path <- fs::path(working, "posts", dest)
Comment on lines +42 to +44
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we try to make sure this is called at root of the quarto project ?

So that if this function is called elsewhere in the project directory we don't create a post folder where we wouldn't want ?

Maybe this check could be in new_blog_post()

  • Checking we are at root of the project
  • If no erroring, or tweaking all path to be from project root maybe.

Related to some thinking at #180 (but this one is really about computing path during knitting)


if (fs::dir_exists(post_path)) {
cli::cli_abort("There is already a {.code {dest}} directory in 'posts/'",
call = call)
} else {
ret <- fs::dir_create(post_path)
}
ret
}

make_post_yaml <- function(title, ...) {
default_values <- list(
title = tools::toTitleCase(title),
author = tools::toTitleCase(whoami::fullname("Your name")),
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could document for .Rd file that whoami is used as default when available

We could also easily make this deps a suggest by using it only if available

tools::toTitleCase(if(rlang::is_installed("whoami")) whoami::fullname("Your name") else "Your name")

date = format(Sys.Date(), "%Y-%m-%d"),
categories = character(0)
)

user_values <- list(...)

yml_values <- utils::modifyList(default_values, user_values)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably I forgot about modifyList as we have a quarto:::merge_list() in the package

if (length(yml_values$categories) == 0) {
yml_values <- yml_values[names(yml_values) != "categories"]
}
yml_values <- yaml::as.yaml(yml_values)
yml_values <- paste0("---\n", yml_values, "---\n")
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, maybe there is a better API.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While dealing with YAML in quarto context we have the issue of yaml R package being 1.1 spec, and Quarto using 1.2 spec.

So it requires some specific handlers as workaround. See

quarto-r/R/utils.R

Lines 6 to 16 in d71b135

write_yaml <- function(x, file) {
handlers <- list(
# Handle yes/no from 1.1 to 1.2
# https://github.com/vubiostat/r-yaml/issues/131
logical = function(x) {
value <- ifelse(x, "true", "false")
structure(value, class = "verbatim")
}
)
yaml::write_yaml(x, file, handlers = handlers)
}

We probably need to adapt for generating not in a file, or refactor the handlers so that we can use them in other functions.

Regaring ---, I believe this is good enough.

This post creation may also be done through templating 🤔 Having a whisker template (or else) to file out with some information. Probably more complex than necessary, but we did that for bookdown skeleton for example using placeholder in template file (https://github.com/rstudio/bookdown/blob/f244cf12bf2c2d7106ac6322b2b2a5796d4ef0c8/R/skeleton.R#L66-L83)

I am fine with current way

yml_values
}

write_post_yaml <- function(x, dest, call) {
dest_file <- fs::path(dest, "index.qmd")
if (fs::file_exists(dest_file)) {
cli::cli_abort("There is already am index.qmd file at {.code {path}}",
call = call)
} else {
ret <- cat(x, file = dest_file)
}
dest_file
}
44 changes: 44 additions & 0 deletions man/new_blog_post.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions tests/testthat/_snaps/new-blog-post.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Create a blog post

Code
new_blog_post("Intro to Felt Surrogacy", data = "1999-12-31", open = FALSE)
Condition
Error in `new_blog_post()`:
! There is already a `intro-to-felt-surrogacy` directory in 'posts/'

68 changes: 68 additions & 0 deletions tests/testthat/test-new-blog-post.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
test_that("Create a blog post", {
skip_if_no_quarto("1.4")

current_dir <- getwd()

temp_dir <- withr::local_tempdir()
dir_path <- fs::path(temp_dir, "test-blog-project")

withr::defer(fs::dir_delete(dir_path), envir = rlang::current_env())
Comment on lines +7 to +10
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

withr::local_tempdir() already include a defer call to unlink() recursively its content. So I believe no need for a new defer call


quarto_create_project(name = "test-blog-project", type = "blog",
dir = temp_dir, quiet = TRUE)

setwd(dir_path)
withr::defer(setwd(current_dir), envir = rlang::current_env())
Comment on lines +15 to +16
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Usually I use withr::local_dir() to change working directory for the rest of the test

This should be enough

withr::local_dir(fs::path(temp_dir, "test-blog-project"))


Sys.setenv(FULLNAME="Max Kuhn")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we should keep using withr for that

Suggested change
Sys.setenv(FULLNAME="Max Kuhn")
withr::local_envvar(list(FULLNAME = "Max Kuhn"))


# ------------------------------------------------------------------------------

post_1 <- new_blog_post("Intro to Felt Surrogacy", date = "March 25, 2010",
open = FALSE)
expect_true(fs::file_exists(post_1))
expect_equal(fs::path_file(post_1), "index.qmd")

post_1_dir <- fs::path_split(post_1)[[1]]
post_1_dir <- post_1_dir[length(post_1_dir) - 1]
expect_equal(post_1_dir, "intro-to-felt-surrogacy")

post_1_content <- readLines(post_1)
post_1_content <- paste0(post_1_content, collapse = "\n")
expect_equal(
post_1_content,
"---\ntitle: Intro to Felt Surrogacy\nauthor: Max Kuhn\ndate: March 25, 2010\n---"
)

# ------------------------------------------------------------------------------

expect_snapshot(
new_blog_post("Intro to Felt Surrogacy", data = "1999-12-31", open = FALSE),
error = TRUE
)

# ------------------------------------------------------------------------------

post_2 <-
new_blog_post(
"Intro to Felt Surrogacy",
dest = "The Science of Illusion",
author = "Annie Edison",
date = '2024-04-12',
categories = c("shenanigans", "security"),
open = FALSE)

expect_true(fs::file_exists(post_2))
expect_equal(fs::path_file(post_2), "index.qmd")

post_2_dir <- fs::path_split(post_2)[[1]]
post_2_dir <- post_2_dir[length(post_2_dir) - 1]
expect_equal(post_2_dir, "The Science of Illusion")

post_2_content <- readLines(post_2)
post_2_exp <- c(
"---", "title: Intro to Felt Surrogacy", "author: Annie Edison",
"date: '2024-04-12'", "categories:", "- shenanigans", "- security", "---")
expect_equal(post_2_content, post_2_exp)
})

Loading