Skip to content

Commit aacaff0

Browse files
authored
Merge pull request #643 from carpentries/issue_642
Add better episode yaml frontmatter checking
2 parents 447355d + 83973b4 commit aacaff0

File tree

3 files changed

+72
-25
lines changed

3 files changed

+72
-25
lines changed

DESCRIPTION

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
Package: sandpaper
22
Title: Create and Curate Carpentries Lessons
3-
Version: 0.16.11
3+
Version: 0.16.12.9000
44
Authors@R: c(
55
person(given = "Robert",
66
family = "Davey",
@@ -93,7 +93,8 @@ Imports:
9393
callr,
9494
servr,
9595
utils,
96-
tools
96+
tools,
97+
R.utils
9798
Suggests:
9899
testthat (>= 3.0.0),
99100
covr,

R/utils-yaml.R

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
# Query only the yaml header. This is faster than slurping the entire file...
22
# useful for determining timings :)
33
politely_get_yaml <- function(path) {
4+
file_len <- R.utils::countLines(path)
45
header <- readLines(path, n = 10, encoding = "UTF-8")
6+
57
barriers <- grep("^---$", header)
68
if (length(barriers) == 0) {
79
# we don't need to warn if they are scanning an index.md with no yaml
@@ -12,10 +14,17 @@ politely_get_yaml <- function(path) {
1214
}
1315
return(character(0))
1416
}
17+
18+
# got at least one YAML header in the first 10 lines so check that the first line of a lesson is header open
19+
if (barriers[1] != 1) {
20+
cli::cli_alert_danger("First line is invalid - [expected ---, got {header[1]}] - in episode {path}")
21+
return(character(0))
22+
}
23+
1524
if (length(barriers) == 1) {
1625
to_skip <- 10L
1726
next_ten <- vector(mode = "character", length = 10)
18-
while (length(barriers) < 2) {
27+
while (length(barriers) < 2 && to_skip < file_len) {
1928
next_ten <- scan(
2029
path,
2130
what = character(),
@@ -32,7 +41,31 @@ politely_get_yaml <- function(path) {
3241
to_skip <- to_skip + 10L
3342
}
3443
}
35-
return(header[barriers[1]:barriers[2]])
44+
45+
# validate at the end of scanning
46+
if (is.na(barriers[1]) || is.na(barriers[2]) || barriers[2] <= barriers[1]) {
47+
cli::cli_alert_danger("Cannot find valid open and close of YAML frontmatter in episode {path}")
48+
return(character(0))
49+
}
50+
51+
# if the second line is blank
52+
if (header[1] == "---" && header[2] == "") {
53+
cli::cli_alert_danger("Blank line after first YAML block line in episode {path}")
54+
return(character(0))
55+
}
56+
57+
header <- header[barriers[1]:barriers[2]]
58+
59+
# actually validate the final header
60+
yaml_header <- paste(header, collapse = "\n")
61+
tryCatch({
62+
yaml::yaml.load(yaml_header)
63+
}, error = function(e) {
64+
cli::cli_alert_danger("YAML header is invalid in episode {path}")
65+
return(character(0))
66+
})
67+
68+
return(header)
3669
}
3770

3871
siQuote <- function(string) {

tests/testthat/test-utils-yaml.R

Lines changed: 34 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,24 @@ test_that("siQuote works for normally escaped strings", {
1717
})
1818

1919
cli::test_that_cli("polite yaml throws a message when there is no yaml", {
20-
20+
2121
withr::local_file(tmp <- tempfile())
22+
cat("A malformed YAML header\n---\n", file = tmp)
23+
expect_message(politely_get_yaml(tmp), "First line is invalid")
24+
25+
cat("foo---\nAnother malformed YAML header\n---\n", file = tmp)
26+
expect_message(politely_get_yaml(tmp), "First line is invalid")
27+
28+
cat("---\nYet another malformed YAML header\n# Start of markdown\n\nFoo bar baz\n", file = tmp)
29+
expect_message(politely_get_yaml(tmp), "Cannot find valid open and close of YAML frontmatter")
30+
31+
cat("---\n\nA malformed YAML block\n---\n", file = tmp)
32+
expect_message(politely_get_yaml(tmp), "Blank line after first YAML block line")
33+
2234
cat("# A header\n\nbut no yaml :/\n", file = tmp)
2335
expect_message(politely_get_yaml(tmp), "No yaml header found in the first 10 lines")
2436

37+
2538
})
2639

2740

@@ -30,27 +43,27 @@ test_that("polite yaml works", {
3043
yaml <- "---
3144
a: |
3245
this
33-
34-
46+
47+
3548
is some
36-
37-
38-
39-
40-
41-
42-
43-
49+
50+
51+
52+
53+
54+
55+
56+
4457
poetry?
45-
46-
47-
48-
49-
50-
51-
52-
53-
58+
59+
60+
61+
62+
63+
64+
65+
66+
5467
b: is it?
5568
---
5669
@@ -72,7 +85,7 @@ This is not poetry
7285

7386

7487
test_that("yaml_list() processes nested lists", {
75-
88+
7689
x <- letters[1:3]
7790
nester <- list(b = x, a = list(hello = x, jello = as.list(setNames(x, rev(x)))))
7891
expect_snapshot_output(writeLines(yaml_list(nester)))

0 commit comments

Comments
 (0)