Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
quarto-dev
GitHub Repository: quarto-dev/quarto-cli
Path: blob/main/src/core/html.ts
3562 views
1
/*
2
* html.ts
3
*
4
* Copyright (C) 2020-2022 Posit Software, PBC
5
*
6
*/
7
8
import { Document, Element } from "./deno-dom.ts";
9
10
import { pandocAutoIdentifier } from "./pandoc/pandoc-id.ts";
11
import { isFileRef } from "./http.ts";
12
import { cssFileRefs } from "./css.ts";
13
import { HtmlPostProcessResult } from "../command/render/types.ts";
14
import { warning } from "../deno_ral/log.ts";
15
16
export function asHtmlId(text: string) {
17
return pandocAutoIdentifier(text, false);
18
}
19
20
export function getDecodedAttribute(element: Element, attrib: string) {
21
const value = element.getAttribute(attrib);
22
if (value) {
23
try {
24
return decodeURI(value);
25
} catch (e) {
26
if (e instanceof URIError) {
27
warning(
28
`Invalid URI '${value}' in attribute '${attrib}' of element '${element.tagName}'`,
29
);
30
return value;
31
} else {
32
throw e;
33
}
34
}
35
} else {
36
return value;
37
}
38
}
39
40
const kTagBrackets: Record<string, string> = {
41
"<": "&lt;",
42
">": "&gt;",
43
};
44
45
const kAttrReplacements: Record<string, string> = {
46
'"': "&quot;",
47
"'": "&#039;",
48
...kTagBrackets,
49
"&": "&amp;",
50
};
51
export function encodeAttributeValue(value: unknown) {
52
if (typeof (value) === "string") {
53
let result: string = value as string;
54
Object.keys(kAttrReplacements).forEach((key) => {
55
result = result.replace(key, kAttrReplacements[key]);
56
});
57
return result;
58
} else {
59
return value as string;
60
}
61
}
62
63
export function encodeHtml(value: string) {
64
Object.keys(kTagBrackets).forEach((key) => {
65
value = value.replaceAll(key, kTagBrackets[key]);
66
});
67
return value;
68
}
69
70
export function findParent(
71
el: Element,
72
match: (el: Element) => boolean,
73
): Element | undefined {
74
let targetEl = el;
75
do {
76
if (targetEl.parentElement) {
77
if (match(targetEl.parentElement)) {
78
return targetEl.parentElement;
79
} else {
80
targetEl = targetEl.parentElement;
81
}
82
} else {
83
return undefined;
84
}
85
} while (targetEl !== null && targetEl.nodeType === 1);
86
return undefined;
87
}
88
89
export const kHtmlResourceTags: Record<string, string[]> = {
90
"a": ["href"],
91
"img": ["src", "data-src"],
92
"link": ["href"],
93
"script": ["src"],
94
"embed": ["src"],
95
"iframe": ["src"],
96
"section": ["data-background-image", "data-background-video"],
97
"source": ["src"],
98
};
99
100
export function discoverResourceRefs(
101
doc: Document,
102
): Promise<HtmlPostProcessResult> {
103
// first handle tags
104
const refs: string[] = [];
105
Object.keys(kHtmlResourceTags).forEach((tag) => {
106
for (const attrib of kHtmlResourceTags[tag]) {
107
refs.push(...resolveResourceTag(doc, tag, attrib));
108
}
109
});
110
// css references (import/url)
111
const styles = doc.querySelectorAll("style");
112
for (let i = 0; i < styles.length; i++) {
113
const style = styles[i] as Element;
114
if (style.innerHTML) {
115
refs.push(...cssFileRefs(style.innerHTML));
116
}
117
}
118
return Promise.resolve({ resources: refs, supporting: [] });
119
}
120
121
// in order for tabsets etc to show the right mouse cursor,
122
// we need hrefs in anchor elements to be "empty" instead of missing.
123
// Existing href attributes trigger the any-link pseudo-selector that
124
// browsers set to `cursor: pointer`.
125
export function fixEmptyHrefs(
126
doc: Document,
127
): Promise<HtmlPostProcessResult> {
128
const anchors = doc.querySelectorAll("a");
129
for (let i = 0; i < anchors.length; ++i) {
130
const anchor = anchors[i] as Element;
131
if (!anchor.getAttribute("href")) {
132
anchor.setAttribute("href", "");
133
}
134
}
135
return Promise.resolve({
136
resources: [],
137
supporting: [],
138
});
139
}
140
141
export function processFileResourceRefs(
142
doc: Document,
143
tag: string,
144
attrib: string,
145
onRef: (tag: Element, ref: string) => void,
146
) {
147
const tags = doc.querySelectorAll(tag);
148
for (let i = 0; i < tags.length; i++) {
149
const tag = tags[i] as Element;
150
const href = getDecodedAttribute(tag, attrib);
151
if (href !== null && href.length > 0 && isFileRef(href)) {
152
onRef(tag, href);
153
}
154
}
155
}
156
157
function resolveResourceTag(
158
doc: Document,
159
tag: string,
160
attrib: string,
161
) {
162
const refs: string[] = [];
163
processFileResourceRefs(
164
doc,
165
tag,
166
attrib,
167
(_tag: Element, ref: string) => refs.push(ref),
168
);
169
return refs;
170
}
171
172
export function placeholderHtml(context: string, html: string) {
173
return `${beginPlaceholder(context)}\n${html}\n${endPlaceholder(context)}`;
174
}
175
176
export function fillPlaceholderHtml(
177
html: string,
178
context: string,
179
content: string,
180
) {
181
const begin = beginPlaceholder(context);
182
const beginPos = html.indexOf(begin);
183
const end = endPlaceholder(context);
184
const endPos = html.indexOf(end);
185
186
if (beginPos !== -1 && endPos !== -1) {
187
return html.slice(0, beginPos + begin.length) + "\n" + content + "\n" +
188
html.slice(endPos);
189
} else {
190
return html;
191
}
192
}
193
194
export function preservePlaceholders(
195
html: string,
196
) {
197
const placeholders = new Map<string, string>();
198
html = html.replaceAll(/<!--\/?quarto-placeholder-.*?-->/g, (match) => {
199
const id = globalThis.crypto.randomUUID();
200
placeholders.set(id, match);
201
return id;
202
});
203
return { html, placeholders };
204
}
205
206
export function restorePlaceholders(
207
html: string,
208
placeholders: Map<string, string>,
209
) {
210
placeholders.forEach((value, key) => {
211
html = html.replace(key, value);
212
});
213
return html;
214
}
215
216
function beginPlaceholder(context: string) {
217
return `<!--${placeholderTag(context)}-->`;
218
}
219
220
function endPlaceholder(context: string) {
221
return `<!--/${placeholderTag(context)}-->`;
222
}
223
224
function placeholderTag(context: string) {
225
return `quarto-placeholder-${context}`;
226
}
227
228