Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
quarto-dev
GitHub Repository: quarto-dev/quarto-cli
Path: blob/main/src/command/create/artifacts/extension.ts
6446 views
1
/*
2
* extension.ts
3
*
4
* Copyright (C) 2020-2022 Posit Software, PBC
5
*/
6
7
import {
8
ArtifactCreator,
9
CreateContext,
10
CreateDirective,
11
} from "../cmd-types.ts";
12
13
import { ejsData, renderAndCopyArtifacts } from "./artifact-shared.ts";
14
15
import { resourcePath } from "../../../core/resources.ts";
16
17
import { Input, Select } from "cliffy/prompt/mod.ts";
18
import { dirname, join } from "../../../deno_ral/path.ts";
19
import { copySync, ensureDirSync, existsSync } from "../../../deno_ral/fs.ts";
20
21
const kType = "type";
22
const kSubType = "subtype";
23
const kName = "name";
24
const kCellLanguage = "cellLanguage";
25
26
const kTypeExtension = "extension";
27
28
interface ExtensionType {
29
name: string;
30
value: string;
31
openfiles: string[];
32
}
33
34
const kExtensionTypes: Array<string | ExtensionType> = [
35
{ name: "shortcode", value: "shortcode", openfiles: ["example.qmd"] },
36
{ name: "filter", value: "filter", openfiles: ["example.qmd"] },
37
{
38
name: "revealjs plugin",
39
value: "revealjs-plugin",
40
openfiles: ["example.qmd"],
41
},
42
{ name: "journal format", value: "journal", openfiles: ["template.qmd"] },
43
{ name: "custom format", value: "format", openfiles: ["template.qmd"] },
44
{ name: "metadata", value: "metadata", openfiles: [] },
45
{ name: "brand", value: "brand", openfiles: [] },
46
{ name: "engine", value: "engine", openfiles: ["example.qmd"] },
47
];
48
49
const kExtensionSubtypes: Record<string, string[]> = {
50
"format": ["html", "pdf", "docx", "revealjs", "typst"],
51
};
52
53
const kExtensionValues = kExtensionTypes.filter((t) => typeof t === "object")
54
.map((t) => (t as { name: string; value: string }).value);
55
56
export const extensionArtifactCreator: ArtifactCreator = {
57
displayName: "Extension",
58
type: kTypeExtension,
59
resolveOptions,
60
finalizeOptions,
61
nextPrompt,
62
createArtifact,
63
};
64
65
function resolveOptions(args: string[]): Record<string, unknown> {
66
// The first argument is the extension type
67
// The second argument is the name
68
// The third argument is the cell language (for engine extensions)
69
const typeRaw = args.length > 0 ? args[0] : undefined;
70
const nameRaw = args.length > 1 ? args[1] : undefined;
71
const cellLanguageRaw = args.length > 2 ? args[2] : undefined;
72
73
const options: Record<string, unknown> = {};
74
75
// Populate the type data
76
if (typeRaw) {
77
const [type, template] = typeRaw.split(":");
78
options[kType] = type;
79
options[kSubType] = template;
80
}
81
82
// Populate a directory, if provided
83
if (nameRaw) {
84
options[kName] = nameRaw;
85
}
86
87
// For engine type, populate the cell language if provided
88
if (cellLanguageRaw && options[kType] === "engine") {
89
options[kCellLanguage] = cellLanguageRaw;
90
}
91
92
return options;
93
}
94
95
function finalizeOptions(createOptions: CreateContext) {
96
// There should be a name
97
if (!createOptions.options.name) {
98
throw new Error("Required property 'name' is missing.");
99
}
100
101
// Is the type valid
102
const type = createOptions.options[kType] as string;
103
if (!kExtensionValues.includes(type)) {
104
throw new Error(
105
`The type ${type} isn't valid. Expected one of ${
106
kExtensionValues.join(", ")
107
}`,
108
);
109
}
110
111
// Is the subtype valid
112
const subType = createOptions.options[kSubType] as string;
113
const subTypes = kExtensionSubtypes[type];
114
if (subTypes && !subTypes.includes(subType)) {
115
throw new Error(
116
`The sub type ${subType} isn't valid. Expected one of ${
117
subTypes.join(", ")
118
}`,
119
);
120
}
121
122
// Form a template
123
const template = createOptions.options[kSubType]
124
? `${createOptions.options[kType]}:${createOptions.options[kSubType]}`
125
: createOptions.options[kType];
126
127
// Provide a directory and title
128
return {
129
displayType: "extension",
130
name: createOptions.options[kName],
131
directory: join(
132
createOptions.cwd,
133
createOptions.options[kName] as string,
134
),
135
template,
136
options: createOptions.options,
137
} as CreateDirective;
138
}
139
140
function nextPrompt(
141
createOptions: CreateContext,
142
) {
143
// First ensure that there is a type
144
if (!createOptions.options[kType]) {
145
return {
146
name: kType,
147
message: "Type",
148
type: Select,
149
options: kExtensionTypes.map((t) => {
150
if (t === "---") {
151
return Select.separator("--------");
152
} else {
153
return t;
154
}
155
}),
156
};
157
}
158
159
const subTypes = kExtensionSubtypes[createOptions.options[kType] as string];
160
if (
161
!createOptions.options[kSubType] &&
162
subTypes && subTypes.length > 0
163
) {
164
return {
165
name: kSubType,
166
message: "Base Format",
167
type: Select,
168
options: subTypes.map((t) => {
169
return {
170
name: t,
171
value: t,
172
};
173
}),
174
};
175
}
176
177
// Collect a title
178
if (!createOptions.options[kName]) {
179
return {
180
name: kName,
181
message: "Extension Name",
182
type: Input,
183
};
184
}
185
186
// Collect cell language for engine extensions
187
if (
188
createOptions.options[kType] === "engine" &&
189
!createOptions.options[kCellLanguage]
190
) {
191
return {
192
name: kCellLanguage,
193
message: "Default cell language name",
194
type: Input,
195
default: createOptions.options[kName] as string,
196
};
197
}
198
}
199
200
function typeFromTemplate(template: string) {
201
return template.split(":")[0];
202
}
203
204
async function createArtifact(
205
createDirective: CreateDirective,
206
quiet?: boolean,
207
) {
208
// Find the type using the template
209
const createType = typeFromTemplate(createDirective.template);
210
const extType = kExtensionTypes.find((type) => {
211
if (typeof type === "object") {
212
return type.value === createType;
213
} else {
214
return false;
215
}
216
});
217
if (!extType) {
218
throw new Error(`Unrecognized extension type ${createType}`);
219
}
220
const openfiles = extType ? (extType as ExtensionType).openfiles : [];
221
222
// Create the extension
223
await createExtension(createDirective, quiet);
224
return {
225
path: createDirective.directory,
226
openfiles,
227
};
228
}
229
230
async function createExtension(
231
createDirective: CreateDirective,
232
quiet?: boolean,
233
) {
234
// The folder for this extension
235
const artifact = templateFolder(createDirective);
236
237
// The target directory
238
const target = createDirective.directory;
239
240
if (existsSync(target)) {
241
// The target directory already exists
242
throw new Error(
243
`The directory ${target} already exists. Quarto extensions must have unique names - please modify the existing extension or use a unique name.`,
244
);
245
}
246
247
// Data for this extension
248
const data = await ejsData(createDirective);
249
250
// Render or copy the artifact
251
const filesCreated = renderAndCopyArtifacts(
252
target,
253
artifact,
254
createDirective,
255
data,
256
quiet,
257
);
258
259
return filesCreated[0];
260
}
261
262
function templateFolder(createDirective: CreateDirective) {
263
const basePath = resourcePath(join("create", "extensions"));
264
const artifactFolderName = createDirective.template.replace(":", "-");
265
return join(basePath, artifactFolderName);
266
}
267
268