diff --git a/news/changelog-1.3.md b/news/changelog-1.3.md
index b31b0f88e5c..9c0316f1d6c 100644
--- a/news/changelog-1.3.md
+++ b/news/changelog-1.3.md
@@ -101,6 +101,10 @@
- Improve the performance of extremely large documents with margin elements by improving the efficiency of positioning the elements.
+## Docx Format
+
+- Ensure that the figure caption and the figure itself is laid out as consecutive paragraphs. ([#4004](https://github.com/quarto-dev/quarto-cli/issues/4004))
+
## Listings
- Listings now support `template-params`, which will be passed to custom EJS templates in a variable called `templateParams` when a listing is rendered.
diff --git a/src/resources/filters/layout/wp.lua b/src/resources/filters/layout/wp.lua
index e54637378be..3bd2b93b706 100644
--- a/src/resources/filters/layout/wp.lua
+++ b/src/resources/filters/layout/wp.lua
@@ -8,46 +8,40 @@ function tableWpPanel(divEl, layout, caption)
})
end
-
function wpDivFigure(div)
-
- -- options
- options = {
- pageWidth = wpPageWidth(),
- }
- -- determine divCaption handler (always left-align)
- local divCaption = nil
- if _quarto.format.isDocxOutput() then
- divCaption = docxDivCaption
- elseif _quarto.format.isOdtOutput() then
- divCaption = odtDivCaption
- end
- if divCaption then
- options.divCaption = function(el, align) return divCaption(el, "left") end
- end
-
- -- get alignment
local align = figAlignAttribute(div)
+ local capLoc = capLocation("fig", "bottom")
+
+ local captionPara = div.content[2]:clone()
+ local figurePara = div.content[1]:clone()
+
+ -- Switch to modern alignment directives for OOXML
+ local wordAligns = {
+ left = "start",
+ right = "end",
+ center = "center"
+ }
+
+ -- Generate a raw OOXML string that sets paragraph properties
+ local docxAlign = ""
- -- create the row/cell for the figure
- local row = pandoc.List()
- local cell = div:clone()
- transferImageWidthToCell(div, cell)
- row:insert(tableCellContent(cell, align, options))
-
- -- make the table
- local figureTable = pandoc.SimpleTable(
- pandoc.List(), -- caption
- { layoutTableAlign(align) },
- { 1 }, -- full width
- pandoc.List(), -- no headers
- { row } -- figure
- )
-
- -- return it
- return pandoc.utils.from_simple_table(figureTable)
-
+ captionPara.content:insert(1, pandoc.RawInline("openxml", docxAlign))
+
+ if capLoc == "top" then
+
+ return pandoc.Div({
+ captionPara,
+ figurePara
+ })
+
+ else
+ -- "bottom" or default
+ return pandoc.Div({
+ figurePara,
+ captionPara
+ })
+ end
end
function wpPageWidth()
diff --git a/src/resources/filters/quarto-pre/options.lua b/src/resources/filters/quarto-pre/options.lua
index 0a5a986bbe5..b4fd252a79d 100644
--- a/src/resources/filters/quarto-pre/options.lua
+++ b/src/resources/filters/quarto-pre/options.lua
@@ -30,6 +30,15 @@ function var(name, def)
end
end
+function capLocation(scope, default)
+ local loc = option(scope .. '-cap-location', option('cap-location', nil))
+ if loc ~= nil then
+ return inlinesToString(loc)
+ else
+ return default
+ end
+end
+
function parseOption(name, options, def)
local keys = split(name, ".")
diff --git a/tests/docs/smoke-all/2023/03/01/4004.qmd b/tests/docs/smoke-all/2023/03/01/4004.qmd
new file mode 100644
index 00000000000..880295753d7
--- /dev/null
+++ b/tests/docs/smoke-all/2023/03/01/4004.qmd
@@ -0,0 +1,48 @@
+---
+title: "issue 4004"
+format:
+ docx:
+ fig-cap-location: top
+_quarto:
+ tests:
+ docx:
+ ensureDocxRegexMatches:
+ - [
+ Figure 1.*?.*?,
+ Figure 2.*?.*?,
+ Figure 3.*?.*?
+ ]
+---
+
+Lets's see @fig-01. And we'll use different alignments.
+
+```{r}
+#| label: fig-01
+#| fig-cap: The figure caption.
+#| fig-align: left
+plot(1:10)
+```
+
+```{r}
+#| label: fig-02
+#| fig-cap: The figure caption.
+#| fig-align: right
+plot(1:10)
+```
+
+```{r}
+#| label: fig-03
+#| fig-cap: The figure caption.
+#| fig-align: center
+plot(1:10)
+```
+
+Compare with tables (caption implementation not changed for this issue)
+
+```{r}
+#| label: tbl-01
+#| tbl-cap: The table caption.
+library(knitr)
+
+kable(mtcars[1:2, 1:2])
+```