Skip to content

Commit 590278e

Browse files
committed
feat: lint R Markdown out of the box
`.Rmd` is Markdown to the parser already: knitr chunk fences are fenced code blocks, inline `r` expressions are code spans, and the YAML header is frontmatter -- so a `[formats]` remap was all support ever took. The extension is now mapped to Markdown itself, and a full R Markdown document pins the behavior: chunks and inline code are never prose, and everything else is. Signed-off-by: Joseph Kato <joseph@jdkato.io>
1 parent 4d8e228 commit 590278e

5 files changed

Lines changed: 93 additions & 18 deletions

File tree

‎internal/core/format.go‎

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -62,23 +62,23 @@ var FormatByExtension = map[string][]string{
6262
`\.(?:adoc|asciidoc|asc)$`: {".adoc", "markup"},
6363
`\.(?:clj|cljs|cljc|cljd)$`: {".clj", "code"},
6464
`\.(?:cpp|cc|c|cp|cxx|c\+\+|h|hpp|h\+\+)$`: {".cpp", "code"},
65-
`\.(?:css)$`: {".css", "code"},
66-
`\.(?:cs|csx)$`: {".c", "code"},
67-
`\.(?:dita)$`: {".dita", "markup"},
68-
`\.(?:go)$`: {".go", "code"},
69-
`\.(?:hs)$`: {".hs", "code"},
70-
`\.(?:html|htm|shtml|xhtml)$`: {".html", "markup"},
71-
`\.(?:java|bsh)$`: {".java", "code"},
72-
`\.(?:jl)$`: {".jl", "code"},
73-
`\.(?:js|jsx)$`: {".js", "code"},
74-
`\.(?:lua)$`: {".lua", "code"},
75-
`\.(?:md|mdown|markdown|markdn)$`: {".md", "markup"},
76-
`\.(?:mdx)$`: {".mdx", "markup"},
77-
`\.(?:org)$`: {".org", "markup"},
78-
`\.(?:php)$`: {".php", "code"},
79-
`\.(?:pl|pm|pod)$`: {".r", "code"},
80-
`\.(?:proto)$`: {".proto", "code"},
81-
`\.(?:ps1|psm1|psd1)$`: {".ps1", "code"},
65+
`\.(?:css)$`: {".css", "code"},
66+
`\.(?:cs|csx)$`: {".c", "code"},
67+
`\.(?:dita)$`: {".dita", "markup"},
68+
`\.(?:go)$`: {".go", "code"},
69+
`\.(?:hs)$`: {".hs", "code"},
70+
`\.(?:html|htm|shtml|xhtml)$`: {".html", "markup"},
71+
`\.(?:java|bsh)$`: {".java", "code"},
72+
`\.(?:jl)$`: {".jl", "code"},
73+
`\.(?:js|jsx)$`: {".js", "code"},
74+
`\.(?:lua)$`: {".lua", "code"},
75+
`\.(?:md|mdown|markdown|markdn|[Rr]md)$`: {".md", "markup"},
76+
`\.(?:mdx)$`: {".mdx", "markup"},
77+
`\.(?:org)$`: {".org", "markup"},
78+
`\.(?:php)$`: {".php", "code"},
79+
`\.(?:pl|pm|pod)$`: {".r", "code"},
80+
`\.(?:proto)$`: {".proto", "code"},
81+
`\.(?:ps1|psm1|psd1)$`: {".ps1", "code"},
8282
`\.(?:rb|Gemfile|Rakefile|Brewfile|gemspec)$`: {".rb", "code"},
8383
`\.(?:rs)$`: {".rs", "code"},
8484
`\.(?:rst|rest)$`: {".rst", "markup"},

‎internal/core/util_test.go‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ func TestFormatFromExt(t *testing.T) {
1313
".py": {".py", "code"},
1414
".cxx": {".cpp", "code"},
1515
".mdown": {".md", "markup"},
16+
".Rmd": {".md", "markup"},
17+
".rmd": {".md", "markup"},
18+
".R": {".r", "code"},
1619
}
1720
m := map[string]string{}
1821
for ext, format := range extToFormat {

‎testdata/e2e/config-flags.yaml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ cases:
8787
content/c.md:1:7:Vale.Spelling:Did you really mean 'ipsum'?
8888
8989
- name: glob-negated-multi
90-
args: "--glob=!**/*.{md,py,mdx} ."
90+
args: "--glob=!**/*.{md,py,mdx,Rmd} ."
9191
exit: 0
9292
absent:
9393
- md

‎testdata/e2e/lint.yaml‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,19 @@ cases:
191191
test.md:44:5:vale.Annotations:'TODO' left in text
192192
test.md:46:3:vale.Annotations:'TODO' left in text
193193
194+
- name: rmarkdown
195+
about: .Rmd is Markdown out of the box -- knitr chunk fences and inline
196+
`r` spans are code, and everything else lints as prose
197+
args: test.Rmd
198+
exit: 0
199+
want: |
200+
test.Rmd:19:47:vale.Annotations:'TODO' left in text
201+
test.Rmd:35:25:vale.Annotations:'XXX' left in text
202+
test.Rmd:52:23:vale.Annotations:'TODO' left in text
203+
test.Rmd:58:32:vale.Annotations:'NOTE' left in text
204+
absent:
205+
- FIXME
206+
194207
- name: assigned-ext
195208
about: --ext overrides the extension
196209
args: "--ext=.md test.xyz"

‎testdata/fixtures/formats/test.Rmd‎

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
---
2+
title: "Quarterly Report"
3+
author: "Jane Doe"
4+
date: "2026-08-07"
5+
output:
6+
html_document:
7+
toc: true
8+
fig_caption: true
9+
bibliography: refs.bib
10+
---
11+
12+
```{r setup, include=FALSE}
13+
knitr::opts_chunk$set(echo = TRUE, warning = FALSE)
14+
library(ggplot2) # FIXME: only loaded for the appendix plots
15+
```
16+
17+
# Overview
18+
19+
This report summarizes the quarterly numbers, TODO confirm the cutoff date.
20+
21+
We read the raw data and clean it:
22+
23+
```{r load-data, message=FALSE}
24+
raw <- read.csv("data/q2.csv")
25+
# FIXME: drop the sentinel rows before release
26+
clean <- raw[raw$value >= 0, ]
27+
```
28+
29+
The clean set has `r nrow(clean)` rows and `r ncol(clean)` columns, and the
30+
running total is `r sum(clean$value) # FIXME`.
31+
32+
## Methods
33+
34+
1. Fit the model with `lm()`.
35+
2. Check the residuals; XXX flag outliers by hand for now.
36+
3. Report $R^2$ and the fitted coefficients.
37+
38+
> Estimates below the detection limit are shown as `<LOD`.
39+
40+
```{r model, echo=FALSE, fig.cap="Fitted values against residuals."}
41+
fit <- lm(value ~ month, data = clean)
42+
plot(fit, which = 1) # XXX: swap for a ggplot version
43+
```
44+
45+
## Results
46+
47+
| Month | Value |
48+
|-------|-------|
49+
| May | 12.4 |
50+
| June | 13.1 |
51+
52+
Table: Monthly means, TODO confirm the June figure.
53+
54+
The slope is `r round(coef(fit)[2], 2)` per month, in line with earlier
55+
work [@smith2021].
56+
57+
::: {.callout-note}
58+
These are preliminary figures; NOTE the caveats in the appendix.
59+
:::

0 commit comments

Comments
 (0)