Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
quarto-dev
GitHub Repository: quarto-dev/quarto-cli
Path: blob/main/src/format/html/format-html-links.ts
6450 views
1
/*
2
* format-html-links.ts
3
*
4
* Copyright (C) 2020-2022 Posit Software, PBC
5
*/
6
7
import {
8
basename,
9
dirname,
10
extname,
11
isAbsolute,
12
relative,
13
} from "../../deno_ral/path.ts";
14
15
import {
16
kDisplayName,
17
kExtensionName,
18
kFormatLinks,
19
kTargetFormat,
20
} from "../../config/constants.ts";
21
import { Format, FormatAliasLink, FormatLink } from "../../config/types.ts";
22
23
import { RenderedFormat } from "../../command/render/types.ts";
24
import {
25
isDocxOutput,
26
isHtmlOutput,
27
isIpynbOutput,
28
isJatsOutput,
29
isMarkdownOutput,
30
isPdfOutput,
31
isPresentationOutput,
32
isTypstOutput,
33
} from "../../config/format.ts";
34
35
export interface AlternateLink {
36
title: string;
37
href: string;
38
icon: string;
39
order: number;
40
dlAttrValue?: string;
41
attr?: Record<string, string>;
42
}
43
44
export function otherFormatLinks(
45
input: string,
46
format: Format,
47
renderedFormats: RenderedFormat[],
48
) {
49
const normalizedFormatLinks = (
50
unnormalizedLinks: unknown,
51
): Array<string | FormatLink | FormatAliasLink> | undefined => {
52
if (typeof unnormalizedLinks === "boolean") {
53
return undefined;
54
} else if (unnormalizedLinks !== undefined) {
55
const linksArr: unknown[] = Array.isArray(unnormalizedLinks)
56
? unnormalizedLinks
57
: [unnormalizedLinks];
58
return linksArr as Array<string | FormatLink | FormatAliasLink>;
59
} else {
60
return undefined;
61
}
62
};
63
const userLinks = normalizedFormatLinks(format.render[kFormatLinks]);
64
65
// Don't include HTML output
66
const filteredFormats = renderedFormats.filter(
67
(renderedFormat) => {
68
return !isHtmlOutput(renderedFormat.format.pandoc, true);
69
},
70
);
71
72
return alternateLinks(
73
input,
74
filteredFormats,
75
userLinks,
76
);
77
}
78
79
export function alternateLinks(
80
input: string,
81
formats: RenderedFormat[],
82
userLinks?: Array<string | FormatLink | FormatAliasLink>,
83
): AlternateLink[] {
84
const alternateLinks: AlternateLink[] = [];
85
86
const alternateLinkForFormat = (
87
renderedFormat: RenderedFormat,
88
order: number,
89
title?: string,
90
icon?: string,
91
) => {
92
const relPath = isAbsolute(renderedFormat.path)
93
? relative(dirname(input), renderedFormat.path)
94
: renderedFormat.path;
95
return {
96
title: `${
97
title ||
98
renderedFormat.format.identifier[kDisplayName] ||
99
renderedFormat.format.pandoc.to
100
}${
101
renderedFormat.format.identifier[kExtensionName]
102
? ` (${renderedFormat.format.identifier[kExtensionName]})`
103
: ""
104
}`,
105
href: relPath,
106
icon: icon || fileBsIconName(renderedFormat.format),
107
order,
108
dlAttrValue: fileDownloadAttr(
109
renderedFormat.format,
110
renderedFormat.path,
111
),
112
};
113
};
114
115
let count = 1;
116
for (const userLink of userLinks || []) {
117
if (typeof userLink === "string") {
118
// We need to filter formats, otherwise, we'll deal
119
// with them below
120
const renderedFormat = formats.find((f) =>
121
f.format.identifier[kTargetFormat] === userLink
122
);
123
if (renderedFormat) {
124
// Just push through
125
alternateLinks.push(alternateLinkForFormat(renderedFormat, count));
126
}
127
} else {
128
const linkObj = userLink as FormatLink | FormatAliasLink;
129
if ("format" in linkObj) {
130
const thatLink = userLink as FormatAliasLink;
131
const rf = formats.find((f) =>
132
f.format.identifier[kTargetFormat] === thatLink.format
133
);
134
if (rf) {
135
// Just push through
136
alternateLinks.push(
137
alternateLinkForFormat(rf, count, thatLink.text, thatLink.icon),
138
);
139
}
140
} else {
141
// This an explicit link
142
const thisLink = userLink as FormatLink;
143
const alternate = {
144
title: thisLink.text,
145
href: thisLink.href,
146
icon: thisLink.icon || fileBsIconForExt(thisLink.href),
147
dlAttrValue: "",
148
order: thisLink.order || count,
149
attr: thisLink.attr,
150
};
151
alternateLinks.push(alternate);
152
}
153
}
154
count++;
155
}
156
157
const userLinksHasFormat = userLinks &&
158
userLinks.some((link) => typeof link === "string");
159
if (!userLinksHasFormat) {
160
formats.forEach((renderedFormat) => {
161
const baseFormat = renderedFormat.format.identifier["base-format"];
162
if (!kExcludeFormatUnlessExplicit.includes(baseFormat || "html")) {
163
alternateLinks.push(alternateLinkForFormat(renderedFormat, count));
164
}
165
count++;
166
});
167
}
168
169
return alternateLinks;
170
}
171
172
// Provides an icon for a format
173
const fileBsIconName = (format: Format) => {
174
if (isDocxOutput(format.pandoc)) {
175
return "file-word";
176
} else if (isPdfOutput(format.pandoc)) {
177
return "file-pdf";
178
} else if (isTypstOutput(format.pandoc)) {
179
return "file-pdf";
180
} else if (isIpynbOutput(format.pandoc)) {
181
return "journal-code";
182
} else if (isMarkdownOutput(format)) {
183
return "file-code";
184
} else if (isPresentationOutput(format.pandoc)) {
185
return "file-slides";
186
} else if (isJatsOutput(format.pandoc)) {
187
return "filetype-xml";
188
} else {
189
return "file";
190
}
191
};
192
193
// Provides a download name for a format/path
194
const fileDownloadAttr = (format: Format, path: string) => {
195
if (isIpynbOutput(format.pandoc)) {
196
return basename(path);
197
} else if (isJatsOutput(format.pandoc)) {
198
return basename(path);
199
} else {
200
return undefined;
201
}
202
};
203
204
const fileBsIconForExt = (path: string) => {
205
const ext = extname(path);
206
switch (ext.toLowerCase()) {
207
case ".docx":
208
return "file-word";
209
case ".pdf":
210
return "file-pdf";
211
case ".ipynb":
212
return "journal-code";
213
case ".md":
214
return "file-code";
215
case ".xml":
216
return "filetype-xml";
217
default:
218
return "file";
219
}
220
};
221
222
const kExcludeFormatUnlessExplicit = ["jats"];
223
224