Skip to content

Commit af32b45

Browse files
author
Al Manning
committed
Merge branch 'main' into feature/site-nesting
2 parents 86724da + 2083b1d commit af32b45

File tree

22 files changed

+1336
-366
lines changed

22 files changed

+1336
-366
lines changed

news/changelog-1.3.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@
3737
- Previously, if the `pdf-engine` was set to `latexmk`, we would bypass many features of Quarto and use Pandoc to produce the PDF output. Starting in in Quarto 1.3, all Quarto features will be enabled for the `latexmk` engine and `latexmk` will be used to run the PDF generation loop.
3838
- Fix author processing in default PDFs for complex author names (#3483)
3939
- Remove excessive vertical space between theorem type blocks ([#3776](https://github.com/quarto-dev/quarto-cli/issues/3776)).
40+
- Fix temporary `.tex` filenames in the presence of multiple variants ([#3762](https://github.com/quarto-dev/quarto-cli/issues/3762)).
41+
- Note that this fix changes the filenames used for PDF files with variants. In quarto 1.3, the automatic output names for PDF files include format variants and modifiers.
4042

4143
## Beamer Format
4244

src/command/render/output-tex.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import { kStdOut, replacePandocOutputArg } from "./flags.ts";
2020
import { OutputRecipe } from "./types.ts";
2121
import { pdfEngine } from "../../config/pdf.ts";
2222
import { execProcess } from "../../core/process.ts";
23+
import { parseFormatString } from "../../core/pandoc/pandoc-formats.ts";
2324

2425
export interface PdfGenerator {
2526
generate: (
@@ -44,7 +45,18 @@ export function texToPdfOutputRecipe(
4445

4546
// there are many characters that give tex trouble in filenames, create
4647
// a target stem that replaces them with the '-' character
47-
const texStem = texSafeFilename(inputStem);
48+
49+
// include variants in the tex stem if they are present to avoid
50+
// overwriting files
51+
let fixupInputName = "";
52+
if (format.identifier["target-format"]) {
53+
const formatDesc = parseFormatString(format.identifier["target-format"]);
54+
fixupInputName = `${formatDesc.variants.join("")}${
55+
formatDesc.modifiers.join("")
56+
}`;
57+
}
58+
59+
const texStem = texSafeFilename(`${inputStem}${fixupInputName}`);
4860

4961
// cacluate output and args for pandoc (this is an intermediate file
5062
// which we will then compile to a pdf and rename to .tex)

src/core/lib/text.ts

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,14 @@ export function resolveCaseConventionRegex(
196196

197197
// no conventions were specified, we sniff all keys to disallow near-misses
198198
const disallowedNearMisses: string[] = [];
199+
const keySet = new Set(keys);
200+
201+
const addNearMiss = (value: string) => {
202+
if (!keySet.has(value)) {
203+
disallowedNearMisses.push(value);
204+
}
205+
};
206+
199207
const foundConventions: Set<CaseConvention> = new Set();
200208
for (const key of keys) {
201209
const found = detectCaseConvention(key);
@@ -204,19 +212,16 @@ export function resolveCaseConventionRegex(
204212
}
205213
switch (found) {
206214
case "capitalizationCase":
207-
disallowedNearMisses.push(toUnderscoreCase(key), toDashCase(key));
215+
addNearMiss(toUnderscoreCase(key));
216+
addNearMiss(toDashCase(key));
208217
break;
209218
case "dash-case":
210-
disallowedNearMisses.push(
211-
toUnderscoreCase(key),
212-
toCapitalizationCase(key),
213-
);
219+
addNearMiss(toUnderscoreCase(key));
220+
addNearMiss(toCapitalizationCase(key));
214221
break;
215222
case "underscore_case":
216-
disallowedNearMisses.push(
217-
toDashCase(key),
218-
toCapitalizationCase(key),
219-
);
223+
addNearMiss(toDashCase(key));
224+
addNearMiss(toCapitalizationCase(key));
220225
break;
221226
}
222227
}

src/core/lib/yaml-intelligence/annotated-yaml.ts

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -652,3 +652,70 @@ export function locateAnnotation(
652652
}
653653
return annotation;
654654
}
655+
656+
// this supports AnnotatedParse results built
657+
// from deno yaml as well as tree-sitter.
658+
export function navigate(
659+
path: (number | string)[],
660+
annotation: AnnotatedParse | undefined,
661+
returnKey = false, // if true, then return the *key* entry as the final result rather than the *value* entry.
662+
pathIndex = 0,
663+
): AnnotatedParse | undefined {
664+
// this looks a little strange, but it's easier to catch the error
665+
// here than in the different cases below
666+
if (annotation === undefined) {
667+
throw new Error("Can't navigate an undefined annotation");
668+
}
669+
if (pathIndex >= path.length) {
670+
return annotation;
671+
}
672+
if (annotation.kind === "mapping" || annotation.kind === "block_mapping") {
673+
const { components } = annotation;
674+
const searchKey = path[pathIndex];
675+
// this loop is inverted to provide better error messages in the
676+
// case of repeated keys. Repeated keys are an error in any case, but
677+
// the parsing by the validation infrastructure reports the last
678+
// entry of a given key in the mapping as the one that counts
679+
// (instead of the first, which would be what we'd get if running
680+
// the loop forward).
681+
//
682+
// In that case, the validation errors will also point to the last
683+
// entry. In order for the errors to be at least consistent,
684+
// we then loop backwards
685+
const lastKeyIndex = ~~((components.length - 1) / 2) * 2;
686+
for (let i = lastKeyIndex; i >= 0; i -= 2) {
687+
const key = components[i]!.result;
688+
if (key === searchKey) {
689+
if (returnKey && pathIndex === path.length - 1) {
690+
return navigate(path, components[i], returnKey, pathIndex + 1);
691+
} else {
692+
return navigate(path, components[i + 1], returnKey, pathIndex + 1);
693+
}
694+
}
695+
}
696+
return annotation;
697+
// throw new Error(
698+
// `Internal error: searchKey ${searchKey} (path: ${path}) not found in mapping object`,
699+
// );
700+
} else if (
701+
["sequence", "block_sequence", "flow_sequence"].indexOf(annotation.kind) !==
702+
-1
703+
) {
704+
const searchKey = Number(path[pathIndex]);
705+
if (
706+
isNaN(searchKey) || searchKey < 0 ||
707+
searchKey >= annotation.components.length
708+
) {
709+
return annotation;
710+
}
711+
return navigate(
712+
path,
713+
annotation.components[searchKey],
714+
returnKey,
715+
pathIndex + 1,
716+
);
717+
} else {
718+
return annotation;
719+
// throw new Error(`Internal error: unexpected kind ${annotation.kind}`);
720+
}
721+
}

src/core/lib/yaml-schema/common.ts

Lines changed: 108 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -163,40 +163,56 @@ export function objectSchema(params: {
163163
properties = properties || {};
164164
patternProperties = patternProperties || {};
165165
// deno-lint-ignore no-explicit-any
166-
const tags: Record<string, any> = {};
166+
let tags: Record<string, any> = {};
167167
let tagsAreSet = false;
168168
let propertyNames: Schema | undefined = propertyNamesSchema;
169169

170-
const objectKeys = Object.getOwnPropertyNames(completionsParam || properties);
170+
if (completionsParam) {
171+
tags["completions"] = completionsParam;
172+
tagsAreSet = true;
173+
}
171174

172-
if (namingConvention !== "ignore") {
175+
const createCaseConventionSchema = (
176+
props: { [k: string]: Schema },
177+
): StringSchema | undefined => {
178+
// 2023-01-17: we no longer support propertyNames _and_ case convention detection.
179+
// if propertyNames are defined, we don't add case convention detection.
180+
// this simplifies the logic and makes schema debugging easier.
181+
if (namingConvention === "ignore") {
182+
return undefined;
183+
}
184+
const objectKeys = Object.getOwnPropertyNames(
185+
props,
186+
);
173187
const { pattern, list } = resolveCaseConventionRegex(
174188
objectKeys,
175189
namingConvention,
176190
);
177-
if (pattern !== undefined) {
178-
if (propertyNames === undefined) {
179-
propertyNames = {
180-
"type": "string",
181-
pattern,
182-
};
183-
} else {
184-
propertyNames = allOfSchema(
185-
propertyNames,
186-
{
187-
"type": "string",
188-
pattern,
189-
},
190-
);
191-
}
192-
tags["case-convention"] = list;
193-
tagsAreSet = true;
191+
if (pattern === undefined) {
192+
return undefined;
194193
}
195-
}
196-
if (completionsParam) {
197-
tags["completions"] = completionsParam;
198-
tagsAreSet = true;
199-
}
194+
if (propertyNames !== undefined) {
195+
console.error(
196+
"Warning: propertyNames and case convention detection are mutually exclusive.",
197+
);
198+
console.error(
199+
"Add `namingConvention: 'ignore'` to your schema definition to remove this warning.",
200+
);
201+
return undefined;
202+
}
203+
const tags = {
204+
"case-convention": list,
205+
"error-importance": -5,
206+
"case-detection": true,
207+
};
208+
return {
209+
errorMessage: "property ${value} does not match case convention " +
210+
`${objectKeys.join(",")}`,
211+
"type": "string",
212+
pattern,
213+
tags,
214+
};
215+
};
200216

201217
const hasDescription = description !== undefined;
202218
description = description || "be an object";
@@ -237,14 +253,48 @@ export function objectSchema(params: {
237253
result.description = description;
238254
}
239255

256+
const m: Map<string, [Schema, string][]> = new Map();
257+
for (const base of baseSchema) {
258+
for (const [k, v] of Object.entries(base.properties || {})) {
259+
if (!m.has(k)) {
260+
m.set(k, []);
261+
}
262+
// deno-lint-ignore no-explicit-any
263+
m.get(k)!.push([v, (base as any).$id]);
264+
}
265+
}
266+
const errorMsgs = new Set<string>();
267+
for (const [k, l] of m) {
268+
if (l.length > 1) {
269+
errorMsgs.add(
270+
`Internal Error: base schemas ${
271+
l
272+
.map((x) => x[1])
273+
.join(", ")
274+
} share property ${k}.`,
275+
);
276+
}
277+
}
278+
if (errorMsgs.size > 0) {
279+
console.error(
280+
[...errorMsgs].toSorted((a, b) => a.localeCompare(b)).join("\n"),
281+
);
282+
console.error("This is a bug in quarto's schemas.");
283+
console.error(
284+
"Note that we don't throw in order to allow build-js to finish, but the generated schemas will be invalid.",
285+
);
286+
}
287+
240288
result.properties = Object.assign(
241289
{},
242290
...(baseSchema.map((s) => s.properties)),
243291
properties,
244292
);
245293
result.patternProperties = Object.assign(
246294
{},
247-
...(baseSchema.map((s) => s.patternProperties)),
295+
...(baseSchema.map((s) => s.patternProperties).filter((s) =>
296+
s !== undefined
297+
)),
248298
patternProperties,
249299
);
250300

@@ -279,18 +329,46 @@ export function objectSchema(params: {
279329
//
280330
// as a result, we only set propertyNames on the extended schema if both
281331
// all propertyNames fields are defined.
332+
333+
// if we subclass from something with patternProperties that came from case convention
334+
// detection, ignore that.
335+
336+
let filtered = false;
282337
const propNamesArray = baseSchema.map((s) => s.propertyNames)
338+
.filter((s) => {
339+
if (typeof s !== "object") return true;
340+
if (s.tags === undefined) return true;
341+
if (s.tags["case-detection"] === true) {
342+
filtered = true;
343+
return false;
344+
}
345+
return true;
346+
})
283347
.filter((s) => s !== undefined) as Schema[];
284-
if (propertyNames) {
285-
propNamesArray.push(propertyNames);
286-
}
287-
if (propNamesArray.length === baseSchema.length + 1) {
348+
// if (propertyNames) {
349+
// propNamesArray.push(propertyNames);
350+
// }
351+
if (propNamesArray.length === 1) {
352+
result.propertyNames = propNamesArray[0];
353+
} else if (propNamesArray.length > 1) {
288354
result.propertyNames = anyOfSchema(...propNamesArray);
355+
} else {
356+
delete result.propertyNames;
289357
}
290358

291359
// if either of schema or base schema is closed, the derived schema is also closed.
292360
result.closed = closed || baseSchema.some((s) => s.closed);
293361
} else {
362+
const caseConventionSchema = createCaseConventionSchema(properties);
363+
if (caseConventionSchema !== undefined) {
364+
propertyNames = caseConventionSchema;
365+
tags = {
366+
...tags,
367+
...caseConventionSchema.tags,
368+
};
369+
tagsAreSet = true;
370+
}
371+
294372
result = {
295373
...internalId(),
296374
"type": "object",

src/core/lib/yaml-schema/from-yaml.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -285,7 +285,7 @@ function convertFromObject(yaml: any): ConcreteSchema {
285285
const schema = yaml["object"];
286286
// deno-lint-ignore no-explicit-any
287287
const params: Record<string, any> = {};
288-
if (schema.namingConvention) {
288+
if (schema.namingConvention && typeof schema.namingConvention === "string") {
289289
switch (schema.namingConvention) {
290290
case "capitalizationCase":
291291
params.namingConvention = "capitalizationCase";
@@ -349,6 +349,7 @@ function convertFromObject(yaml: any): ConcreteSchema {
349349
default:
350350
throw new Error("Internal Error: this should have failed validation");
351351
}
352+
} else {
352353
params.namingConvention = schema.namingConvention;
353354
}
354355
if (schema.properties) {

src/core/lib/yaml-validation/validator.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,14 @@ class ValidationContext {
153153
// As a last resort, we sort suggestions based on "quality"
154154
const errorTypeQuality = (e: ValidationError): number => {
155155
const t = e.schemaPath.slice().reverse();
156+
if (typeof e.schema === "object") {
157+
if (
158+
e.schema.tags && e.schema.tags["error-importance"] &&
159+
typeof e.schema.tags["error-importance"] === "number"
160+
) {
161+
return e.schema.tags["error-importance"];
162+
}
163+
}
156164
if (e.schemaPath.indexOf("propertyNames") !== -1) {
157165
// suggesting invalid property names is bad if there are other errors to report
158166
return 10;

0 commit comments

Comments
 (0)