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