Skip to content
Draft
Show file tree
Hide file tree
Changes from all 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
4 changes: 3 additions & 1 deletion src/command/render/output-tex.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
kKeepTex,
kOutputExt,
kOutputFile,
kOutputSuffix,
kTargetFormat,
} from "../../config/constants.ts";
import { Format } from "../../config/types.ts";
Expand Down Expand Up @@ -61,7 +62,8 @@ export function texToPdfOutputRecipe(
}`;
}

const texStem = texSafeFilename(`${inputStem}${fixupInputName}`);
const suffix = format.render[kOutputSuffix] || "";
const texStem = texSafeFilename(`${inputStem}${suffix}${fixupInputName}`);

// calculate output and args for pandoc (this is an intermediate file
// which we will then compile to a pdf and rename to .tex)
Expand Down
8 changes: 5 additions & 3 deletions src/command/render/output-typst.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
kKeepTyp,
kOutputExt,
kOutputFile,
kOutputSuffix,
kVariant,
} from "../../config/constants.ts";
import { Format } from "../../config/types.ts";
Expand Down Expand Up @@ -45,7 +46,8 @@ export function typstPdfOutputRecipe(
// calculate output and args for pandoc (this is an intermediate file
// which we will then compile to a pdf and rename to .typ)
const [inputDir, inputStem] = dirAndStem(input);
const output = inputStem + ".typ";
const suffix = format.render[kOutputSuffix] || "";
const output = inputStem + suffix + ".typ";
let args = options.pandocArgs || [];
const pandoc = { ...format.pandoc };
if (options.flags?.output) {
Expand All @@ -62,7 +64,7 @@ export function typstPdfOutputRecipe(

// run typst
await validateRequiredTypstVersion();
const pdfOutput = join(inputDir, inputStem + ".pdf");
const pdfOutput = join(inputDir, inputStem + suffix + ".pdf");
const typstOptions: TypstCompileOptions = {
quiet: options.flags?.quiet,
fontPaths: asArray(format.metadata?.[kFontPaths]) as string[],
Expand Down Expand Up @@ -111,7 +113,7 @@ export function typstPdfOutputRecipe(
? finalOutput === kStdOut
? undefined
: normalizeOutputPath(input, finalOutput)
: normalizeOutputPath(input, join(inputDir, inputStem + ".pdf"));
: normalizeOutputPath(input, join(inputDir, inputStem + suffix + ".pdf"));

// return recipe
const recipe: OutputRecipe = {
Expand Down
10 changes: 7 additions & 3 deletions src/command/render/output.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {
import {
kOutputExt,
kOutputFile,
kOutputSuffix,
kPreserveYaml,
kVariant,
} from "../../config/constants.ts";
Expand Down Expand Up @@ -184,18 +185,21 @@ export function outputRecipe(
const deriveAutoOutput = () => {
// no output specified: derive an output path from the extension

// Get the suffix if specified
const suffix = format.render[kOutputSuffix] || "";

// derive new output file
let output = inputStem + "." + ext;
let output = `${inputStem}${suffix}.${ext}`;
// special case for .md to .md, need to append the writer to create a
// non-conflicting filename
if (extname(input) === ".md" && ext === "md") {
output = `${inputStem}-${format.identifier["base-format"]}.md`;
output = `${inputStem}${suffix}-${format.identifier["base-format"]}.md`;
}

// special case if the source will overwrite the destination (note: this
// behavior can be customized with a custom output-ext)
if (output === basename(context.target.source)) {
output = inputStem + `.${kOutExt}.` + ext;
output = `${inputStem}${suffix}.${kOutExt}.${ext}`;
}

// assign output
Expand Down
4 changes: 3 additions & 1 deletion src/command/render/render-contexts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ import {
kMetadataFormat,
kOutputExt,
kOutputFile,
kOutputSuffix,
kServer,
kTargetFormat,
kWarning,
Expand Down Expand Up @@ -330,7 +331,8 @@ export async function renderFormats(
// resolve output-file
if (!format.pandoc[kOutputFile]) {
const [_dir, stem] = dirAndStem(file);
format.pandoc[kOutputFile] = `${stem}.${format.render[kOutputExt]}`;
const suffix = format.render[kOutputSuffix] || "";
format.pandoc[kOutputFile] = `${stem}${suffix}.${format.render[kOutputExt]}`;
}
// provide engine
format.execute[kEngine] = context.engine.name;
Expand Down
2 changes: 2 additions & 0 deletions src/config/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ export const kKeepIpynb = "keep-ipynb";
export const kKeepSource = "keep-source";
export const kVariant = "variant";
export const kOutputExt = "output-ext";
export const kOutputSuffix = "output-suffix";
export const kOutputDivs = "output-divs";
export const kPageWidth = "page-width";
export const kFigAlign = "fig-align";
Expand Down Expand Up @@ -191,6 +192,7 @@ export const kRenderDefaultsKeys = [
kClearHiddenClasses,
kVariant,
kOutputExt,
kOutputSuffix,
kOutputDivs,
kPreferHtml,
kPageWidth,
Expand Down
2 changes: 2 additions & 0 deletions src/config/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,7 @@ import {
kOutputDivs,
kOutputExt,
kOutputFile,
kOutputSuffix,
kPageWidth,
kPdfEngine,
kPdfEngineOpt,
Expand Down Expand Up @@ -469,6 +470,7 @@ export interface FormatRender {
[kOutputDivs]?: boolean;
[kVariant]?: string;
[kOutputExt]?: string;
[kOutputSuffix]?: string;
[kPageWidth]?: number;
[kFigAlign]?: "left" | "right" | "center" | "default";
[kFigPos]?: string | null;
Expand Down
15 changes: 14 additions & 1 deletion src/core/render.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* Copyright (C) 2020-2022 Posit Software, PBC
*/

import { kOutputExt, kOutputFile, kServer } from "../config/constants.ts";
import { kOutputExt, kOutputFile, kOutputSuffix, kServer } from "../config/constants.ts";
import { Format, Metadata } from "../config/types.ts";
import { kJupyterEngine, kKnitrEngine } from "../execute/types.ts";
import { dirAndStem } from "./path.ts";
Expand Down Expand Up @@ -47,6 +47,19 @@ export function isServerShinyKnitr(
export function formatOutputFile(format: Format) {
let outputFile = format.pandoc[kOutputFile];
if (outputFile) {
// Apply output-suffix if specified (insert before extension)
const suffix = format.render[kOutputSuffix];
if (suffix) {
const ext = extname(outputFile);
if (ext) {
const stem = outputFile.slice(0, -ext.length);
outputFile = `${stem}${suffix}${ext}`;
} else {
// No extension, just append suffix
outputFile = `${outputFile}${suffix}`;
}
}

if (format.render[kOutputExt]) {
// Don't append the output extension if the same output
// extension is already present. If you update this logic,
Expand Down
7 changes: 7 additions & 0 deletions src/resources/schema/document-render.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,13 @@
description: |
Extension to use for generated output file

- name: output-suffix
schema: string
description: |
Suffix to append to the output filename stem (before the extension).
For example, with `output-suffix: "-slides"`, `presentation.qmd`
becomes `presentation-slides.html`.

- name: template
disabled: [$office-all, ipynb]
schema: path
Expand Down
21 changes: 21 additions & 0 deletions tests/docs/smoke-all/2026/01/10/13111-output-suffix.qmd
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
---
title: "Output Suffix Test"
format:
html:
output-suffix: "-readable"
revealjs:
output-suffix: "-slides"
---

# Test Document

This document tests the `output-suffix` feature.

## Purpose

When rendered with `output-suffix: "-readable"`, this file should produce:
- `13111-output-suffix-readable.html` (from HTML format)
- `13111-output-suffix-slides.html` (from RevealJS format)

This addresses GitHub issue #13111 where using compound extensions like
`.onepage.html` in `output-ext` caused double-extension bugs.
Loading