Skip to content

Commit 7e7c240

Browse files
Allow local extensions to override built-in typst book extension
When a user explicitly specifies `format: orange-book-typst` in a book project, prefer a locally installed copy of the extension over the built-in one, so customizations take effect. The default behavior for `format: typst` (which auto-assigns orange-book) is unchanged. Adds a `preferLocal` flag threaded through the extension lookup chain that, when set, checks local `_extensions/` before built-in extensions. Only the explicit typst book extension case sets this flag. Fixes #14326
1 parent b6e1c2e commit 7e7c240

File tree

8 files changed

+103
-31
lines changed

8 files changed

+103
-31
lines changed

src/command/render/render-contexts.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -678,15 +678,20 @@ const readExtensionFormat = async (
678678
) => {
679679
// Determine effective extension - use default for certain project/format combinations
680680
let effectiveExtension = formatDesc.extension;
681+
let preferLocal = false;
681682

682-
// For book projects with typst format and no explicit extension,
683-
// use orange-book as the default typst book template
684683
if (
685-
!effectiveExtension &&
686684
formatDesc.baseFormat === "typst" &&
687685
project?.config?.project?.[kProjectType] === "book"
688686
) {
689-
effectiveExtension = "orange-book";
687+
if (effectiveExtension) {
688+
// User explicitly named a typst book extension (e.g. format: orange-book-typst),
689+
// prefer a locally installed copy over the built-in so customizations take effect
690+
preferLocal = true;
691+
} else {
692+
// No explicit extension - use orange-book as the default typst book template
693+
effectiveExtension = "orange-book";
694+
}
690695
}
691696

692697
// Read the format file and populate this
@@ -697,6 +702,7 @@ const readExtensionFormat = async (
697702
file,
698703
project?.config,
699704
project?.dir,
705+
preferLocal,
700706
);
701707

702708
// Read the yaml file and resolve / bucketize

src/extension/extension.ts

Lines changed: 54 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -119,9 +119,15 @@ export function createExtensionContext(): ExtensionContext {
119119
input: string,
120120
config?: ProjectConfig,
121121
projectDir?: string,
122+
preferLocal = false,
122123
): Promise<Extension | undefined> => {
123124
// Load the extension and resolve any paths
124-
const unresolved = await loadExtension(name, input, projectDir);
125+
const unresolved = await loadExtension(
126+
name,
127+
input,
128+
projectDir,
129+
preferLocal,
130+
);
125131
return resolveExtensionPaths(unresolved, input, config);
126132
};
127133

@@ -342,9 +348,15 @@ const loadExtension = async (
342348
extension: string,
343349
input: string,
344350
projectDir?: string,
351+
preferLocal = false,
345352
): Promise<Extension> => {
346353
const extensionId = toExtensionId(extension);
347-
const extensionPath = discoverExtensionPath(input, extensionId, projectDir);
354+
const extensionPath = discoverExtensionPath(
355+
input,
356+
extensionId,
357+
projectDir,
358+
preferLocal,
359+
);
348360

349361
if (extensionPath) {
350362
// Find the metadata file, if any
@@ -585,8 +597,9 @@ export function discoverExtensionPath(
585597
input: string,
586598
extensionId: ExtensionId,
587599
projectDir?: string,
600+
preferLocal = false,
588601
) {
589-
const extensionDirGlobs = [];
602+
const extensionDirGlobs: string[] = [];
590603
if (extensionId.organization) {
591604
// If there is an organization, always match that exactly
592605
extensionDirGlobs.push(
@@ -619,6 +632,41 @@ export function discoverExtensionPath(
619632
}
620633
};
621634

635+
const findLocalExtensionDir = () => {
636+
const sourceDir = Deno.statSync(input).isDirectory ? input : dirname(input);
637+
const sourceDirAbs = normalizePath(sourceDir);
638+
639+
if (projectDir && isSubdir(projectDir, sourceDirAbs)) {
640+
let extensionDir;
641+
let currentDir = normalize(sourceDirAbs);
642+
const projDir = normalize(projectDir);
643+
while (!extensionDir) {
644+
extensionDir = findExtensionDir(
645+
join(currentDir, kExtensionDir),
646+
extensionDirGlobs,
647+
);
648+
if (currentDir == projDir) {
649+
break;
650+
}
651+
currentDir = dirname(currentDir);
652+
}
653+
return extensionDir;
654+
} else {
655+
return findExtensionDir(
656+
join(sourceDirAbs, kExtensionDir),
657+
extensionDirGlobs,
658+
);
659+
}
660+
};
661+
662+
// When preferLocal is set, check local project extensions first
663+
if (preferLocal) {
664+
const localDir = findLocalExtensionDir();
665+
if (localDir) {
666+
return localDir;
667+
}
668+
}
669+
622670
// check for built-in
623671
const builtinExtensionDir = findExtensionDir(
624672
builtinExtensions(),
@@ -646,30 +694,9 @@ export function discoverExtensionPath(
646694
}
647695
}
648696

649-
// Start in the source directory
650-
const sourceDir = Deno.statSync(input).isDirectory ? input : dirname(input);
651-
const sourceDirAbs = normalizePath(sourceDir);
652-
653-
if (projectDir && isSubdir(projectDir, sourceDirAbs)) {
654-
let extensionDir;
655-
let currentDir = normalize(sourceDirAbs);
656-
const projDir = normalize(projectDir);
657-
while (!extensionDir) {
658-
extensionDir = findExtensionDir(
659-
join(currentDir, kExtensionDir),
660-
extensionDirGlobs,
661-
);
662-
if (currentDir == projDir) {
663-
break;
664-
}
665-
currentDir = dirname(currentDir);
666-
}
667-
return extensionDir;
668-
} else {
669-
return findExtensionDir(
670-
join(sourceDirAbs, kExtensionDir),
671-
extensionDirGlobs,
672-
);
697+
// Check local project extensions (when not already checked via preferLocal)
698+
if (!preferLocal) {
699+
return findLocalExtensionDir();
673700
}
674701
}
675702

src/extension/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ export interface ExtensionContext {
7171
input: string,
7272
config?: ProjectConfig,
7373
projectDir?: string,
74+
preferLocal?: boolean,
7475
): Promise<Extension | undefined>;
7576
find(
7677
name: string,
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
/.quarto/
2+
**/*.quarto_ipynb
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
title: Override Test
2+
author: Test
3+
version: 0.1.0
4+
contributes:
5+
formats:
6+
typst:
7+
template-partials:
8+
- typst-show.typ
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
// LOCAL-OVERRIDE-MARKER
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
project:
2+
type: book
3+
4+
book:
5+
title: "Override Test"
6+
author: "Test Author"
7+
date: "2024-01-01"
8+
chapters:
9+
- index.qmd
10+
11+
format:
12+
orange-book-typst:
13+
keep-typ: true
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
---
2+
keep-typ: true
3+
_quarto:
4+
render-project: true
5+
tests:
6+
orange-book-typst:
7+
ensureTypstFileRegexMatches:
8+
- # must match
9+
- "LOCAL-OVERRIDE-MARKER"
10+
---
11+
12+
# Introduction
13+
14+
Hello world.

0 commit comments

Comments
 (0)