Skip to content

Commit 944e3fd

Browse files
authored
Merge branch 'main' into fix/issue14249
2 parents 5b7ab17 + e626825 commit 944e3fd

18 files changed

Lines changed: 1955 additions & 1641 deletions

File tree

news/changelog-1.10.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,25 @@
11
All changes included in 1.10:
22

3+
## Regression fixes
4+
5+
- ([#14267](https://github.com/quarto-dev/quarto-cli/issues/14267)): Fix Windows paths with accented characters (e.g., `C:\Users\Sébastien\`) breaking dart-sass compilation.
6+
37
## Formats
48

9+
### `typst`
10+
11+
- ([#14261](https://github.com/quarto-dev/quarto-cli/issues/14261)): Fix theorem/example block titles containing inline code producing invalid Typst markup when syntax highlighting is applied.
12+
513
### `revealjs`
614

715
- ([#14249](https://github.com/quarto-dev/quarto-cli/issues/14249)): Add `aria-label` to slide-menu button link for screen reader accessibility. (author: @mcanouil)
16+
17+
## Commands
18+
19+
### `quarto create`
20+
21+
- ([#14250](https://github.com/quarto-dev/quarto-cli/issues/14250)): Fix `quarto create` producing read-only files when Quarto is installed via system packages (e.g., `.deb`). Files copied from installed resources now have user-write permission ensured.
22+
23+
## Other fixes and improvements
24+
25+
- ([#6651](https://github.com/quarto-dev/quarto-cli/issues/6651)): Fix dart-sass compilation failing in enterprise environments where `.bat` files are blocked by group policy.

src/command/create/artifacts/artifact-shared.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,11 @@ import { gfmAutoIdentifier } from "../../../core/pandoc/pandoc-id.ts";
1212
import { coerce } from "semver/mod.ts";
1313
import { info } from "../../../deno_ral/log.ts";
1414
import { basename, dirname, join, relative } from "../../../deno_ral/path.ts";
15-
import { ensureDirSync, walkSync } from "../../../deno_ral/fs.ts";
15+
import {
16+
ensureDirSync,
17+
ensureUserWritable,
18+
walkSync,
19+
} from "../../../deno_ral/fs.ts";
1620
import { renderEjs } from "../../../core/ejs.ts";
1721
import { safeExistsSync } from "../../../core/path.ts";
1822
import { CreateDirective, CreateDirectiveData } from "../cmd-types.ts";
@@ -116,6 +120,7 @@ const renderArtifact = (
116120
}
117121
ensureDirSync(dirname(target));
118122
Deno.copyFileSync(src, target);
123+
ensureUserWritable(target);
119124
return target;
120125
}
121126
};

src/core/dart-sass.ts

Lines changed: 67 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,18 @@
11
/*
22
* dart-sass.ts
33
*
4-
* Copyright (C) 2020-2022 Posit Software, PBC
4+
* Copyright (C) 2020-2025 Posit Software, PBC
55
*/
66
import { join } from "../deno_ral/path.ts";
77

88
import { architectureToolsPath } from "./resources.ts";
99
import { execProcess } from "./process.ts";
10-
import { ProcessResult } from "./process-types.ts";
1110
import { TempContext } from "./temp.ts";
1211
import { lines } from "./text.ts";
1312
import { debug, info } from "../deno_ral/log.ts";
1413
import { existsSync } from "../deno_ral/fs.ts";
1514
import { warnOnce } from "./log.ts";
1615
import { isWindows } from "../deno_ral/platform.ts";
17-
import { requireQuoting, safeWindowsExec } from "./windows.ts";
1816

1917
export function dartSassInstallDir() {
2018
return architectureToolsPath("dart-sass");
@@ -60,80 +58,91 @@ export async function dartCompile(
6058
*/
6159
export interface DartCommandOptions {
6260
/**
63-
* Override the sass executable path.
64-
* Primarily used for testing with spaced paths.
61+
* Override the dart-sass install directory.
62+
* Used for testing with non-standard paths (spaces, accented characters).
6563
*/
66-
sassPath?: string;
64+
installDir?: string;
6765
}
6866

69-
export async function dartCommand(
70-
args: string[],
71-
options?: DartCommandOptions,
72-
) {
73-
const resolvePath = () => {
67+
/**
68+
* Resolve the dart-sass command and its base arguments.
69+
*
70+
* On Windows, calls dart.exe + sass.snapshot directly instead of going
71+
* through sass.bat. The bundled sass.bat is a thin wrapper generated by
72+
* dart_cli_pkg that just runs:
73+
* "%SCRIPTPATH%\src\dart.exe" "%SCRIPTPATH%\src\sass.snapshot" %arguments%
74+
*
75+
* Template source:
76+
* https://github.com/google/dart_cli_pkg/blob/main/lib/src/templates/standalone/executable.bat.mustache
77+
* Upstream issue to ship standalone .exe instead of .bat + dart.exe:
78+
* https://github.com/google/dart_cli_pkg/issues/67
79+
*
80+
* Bypassing sass.bat avoids multiple .bat file issues on Windows:
81+
* - Deno quoting bugs with spaced paths (#13997)
82+
* - cmd.exe OEM code page misreading UTF-8 accented paths (#14267)
83+
* - Enterprise group policy blocking .bat execution (#6651)
84+
*/
85+
function resolveSassCommand(options?: DartCommandOptions): {
86+
cmd: string;
87+
baseArgs: string[];
88+
} {
89+
const installDir = options?.installDir;
90+
if (installDir == null) {
91+
// Only check env var override when no explicit installDir is provided.
92+
// If QUARTO_DART_SASS doesn't exist on disk, fall through to use the
93+
// bundled dart-sass at the default architectureToolsPath.
7494
const dartOverrideCmd = Deno.env.get("QUARTO_DART_SASS");
7595
if (dartOverrideCmd) {
7696
if (!existsSync(dartOverrideCmd)) {
7797
warnOnce(
7898
`Specified QUARTO_DART_SASS does not exist, using built in dart sass.`,
7999
);
80100
} else {
81-
return dartOverrideCmd;
101+
return { cmd: dartOverrideCmd, baseArgs: [] };
82102
}
83103
}
104+
}
84105

85-
const command = isWindows ? "sass.bat" : "sass";
86-
return architectureToolsPath(join("dart-sass", command));
87-
};
88-
const sass = options?.sassPath ?? resolvePath();
89-
90-
// Process result helper (shared by Windows and non-Windows paths)
91-
const processResult = (result: ProcessResult): string | undefined => {
92-
if (result.success) {
93-
if (result.stderr) {
94-
info(result.stderr);
95-
}
96-
return result.stdout;
97-
} else {
98-
debug(`[DART path] : ${sass}`);
99-
debug(`[DART args] : ${args.join(" ")}`);
100-
debug(`[DART stdout] : ${result.stdout}`);
101-
debug(`[DART stderr] : ${result.stderr}`);
102-
103-
const errLines = lines(result.stderr || "");
104-
// truncate the last 2 lines (they include a pointer to the temp file containing
105-
// all of the concatenated sass, which is more or less incomprehensible for users.
106-
const errMsg = errLines.slice(0, errLines.length - 2).join("\n");
107-
throw new Error("Theme file compilation failed:\n\n" + errMsg);
108-
}
109-
};
106+
const sassDir = installDir ?? architectureToolsPath("dart-sass");
110107

111-
// On Windows, use safeWindowsExec to handle paths with spaces
112-
// (e.g., when Quarto is installed in C:\Program Files\)
113-
// See https://github.com/quarto-dev/quarto-cli/issues/13997
114108
if (isWindows) {
115-
const quoted = requireQuoting([sass, ...args]);
116-
const result = await safeWindowsExec(
117-
quoted.args[0],
118-
quoted.args.slice(1),
119-
(cmd: string[]) => {
120-
return execProcess({
121-
cmd: cmd[0],
122-
args: cmd.slice(1),
123-
stdout: "piped",
124-
stderr: "piped",
125-
});
126-
},
127-
);
128-
return processResult(result);
109+
return {
110+
cmd: join(sassDir, "src", "dart.exe"),
111+
baseArgs: [join(sassDir, "src", "sass.snapshot")],
112+
};
129113
}
130114

131-
// Non-Windows: direct execution
115+
return { cmd: join(sassDir, "sass"), baseArgs: [] };
116+
}
117+
118+
export async function dartCommand(
119+
args: string[],
120+
options?: DartCommandOptions,
121+
) {
122+
const { cmd, baseArgs } = resolveSassCommand(options);
123+
132124
const result = await execProcess({
133-
cmd: sass,
134-
args,
125+
cmd,
126+
args: [...baseArgs, ...args],
135127
stdout: "piped",
136128
stderr: "piped",
137129
});
138-
return processResult(result);
130+
131+
if (result.success) {
132+
if (result.stderr) {
133+
info(result.stderr);
134+
}
135+
return result.stdout;
136+
} else {
137+
debug(`[DART cmd] : ${cmd}`);
138+
debug(`[DART args] : ${[...baseArgs, ...args].join(" ")}`);
139+
debug(`[DART stdout] : ${result.stdout}`);
140+
debug(`[DART stderr] : ${result.stderr}`);
141+
142+
const errLines = lines(result.stderr || "");
143+
// truncate the last 2 lines (they include a pointer to the temp file containing
144+
// all of the concatenated sass, which is more or less incomprehensible for users.
145+
const errMsg = errLines.slice(0, errLines.length - 2).join("\n");
146+
throw new Error("Theme file compilation failed:\n\n" + errMsg);
147+
}
139148
}

src/core/windows.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,7 @@ export async function safeWindowsExec(
156156
try {
157157
Deno.writeTextFileSync(
158158
tempFile,
159-
["@echo off", [program, ...args].join(" ")].join("\n"),
159+
["@echo off", "chcp 65001 >nul", [program, ...args].join(" ")].join("\r\n"),
160160
);
161161
return await fnExec(["cmd", "/c", tempFile]);
162162
} finally {

src/deno_ral/fs.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,3 +191,15 @@ export function safeChmodSync(path: string, mode: number): void {
191191
}
192192
}
193193
}
194+
195+
/**
196+
* Ensure a file has user write permission. Files copied from installed
197+
* resources (e.g. system packages) may be read-only, but users expect
198+
* to edit files created by `quarto create`. No-op on Windows.
199+
*/
200+
export function ensureUserWritable(path: string): void {
201+
const mode = safeModeFromFile(path);
202+
if (mode !== undefined && !(mode & 0o200)) {
203+
safeChmodSync(path, mode | 0o200);
204+
}
205+
}

src/project/project-create.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,11 @@
55
*/
66

77
import * as ld from "../core/lodash.ts";
8-
import { ensureDirSync, existsSync } from "../deno_ral/fs.ts";
8+
import {
9+
ensureDirSync,
10+
ensureUserWritable,
11+
existsSync,
12+
} from "../deno_ral/fs.ts";
913
import { basename, dirname, join } from "../deno_ral/path.ts";
1014
import { info } from "../deno_ral/log.ts";
1115

@@ -139,6 +143,7 @@ export async function projectCreate(options: ProjectCreateOptions) {
139143
if (!existsSync(dest)) {
140144
ensureDirSync(dirname(dest));
141145
copyTo(src, dest);
146+
ensureUserWritable(dest);
142147
if (!options.quiet) {
143148
info("- Created " + displayName, { indent: 2 });
144149
}
@@ -256,6 +261,7 @@ function projectMarkdownFile(
256261
const name = basename(from);
257262
const target = join(dirname(path), name);
258263
copyTo(from, target);
264+
ensureUserWritable(target);
259265
});
260266

261267
return subdirectory ? join(subdirectory, name) : name;

src/quarto.ts

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -196,13 +196,6 @@ export async function quarto(
196196

197197
try {
198198
await promise;
199-
for (const [key, value] of Object.entries(oldEnv)) {
200-
if (value === undefined) {
201-
Deno.env.delete(key);
202-
} else {
203-
Deno.env.set(key, value);
204-
}
205-
}
206199
if (commandFailed()) {
207200
exitWithCleanup(1);
208201
}
@@ -213,6 +206,14 @@ export async function quarto(
213206
} else {
214207
throw e;
215208
}
209+
} finally {
210+
for (const [key, value] of Object.entries(oldEnv)) {
211+
if (value === undefined) {
212+
Deno.env.delete(key);
213+
} else {
214+
Deno.env.set(key, value);
215+
}
216+
}
216217
}
217218
}
218219

src/resources/filters/customnodes/theorem.lua

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -259,9 +259,9 @@ end, function(thm)
259259
ensure_typst_theorems(type)
260260
local preamble = pandoc.Plain({pandoc.RawInline("typst", "#" .. theorem_type.env .. "(")})
261261
if name and #name > 0 then
262-
preamble.content:insert(pandoc.RawInline("typst", 'title: "'))
262+
preamble.content:insert(pandoc.RawInline("typst", 'title: ['))
263263
tappend(preamble.content, name)
264-
preamble.content:insert(pandoc.RawInline("typst", '"'))
264+
preamble.content:insert(pandoc.RawInline("typst", ']'))
265265
end
266266
preamble.content:insert(pandoc.RawInline("typst", ")["))
267267
local callthm = make_scaffold(pandoc.Div, preamble)

tests/docs/smoke-all/crossrefs/theorem/algorithm.qmd

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ _quarto:
2222
-
2323
- "#ref\\(<alg-gcd>, supplement: \\[alg.\\]\\)"
2424
- "#ref\\(<alg-gcd>, supplement: \\[Alg.\\]\\)"
25-
- "#algorithm\\(title: \"Euclid\"\\)"
25+
- "#algorithm\\(title: \\[Euclid\\]\\)"
2626
- []
2727
markdown:
2828
ensureFileRegexMatches:

tests/docs/smoke-all/crossrefs/theorem/lemma-1.qmd

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ _quarto:
2020
ensureTypstFileRegexMatches:
2121
-
2222
- "#ref\\(<lem-line>, supplement: \\[Lemma\\]\\)"
23-
- "#lemma\\(title: \"Line\"\\)"
23+
- "#lemma\\(title: \\[Line\\]\\)"
2424
- []
2525
markdown:
2626
ensureFileRegexMatches:

0 commit comments

Comments
 (0)