Skip to content

Commit f38a78d

Browse files
authored
Merge pull request #11397 from quarto-dev/fix/utf8-categories
listing - Correctly handle non ASCII category and special characters
2 parents 73f8183 + c4f393b commit f38a78d

File tree

14 files changed

+82
-13
lines changed

14 files changed

+82
-13
lines changed

src/command/render/pandoc.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,7 @@ import {
207207
BrandFontFile,
208208
BrandFontGoogle,
209209
} from "../../resources/types/schema-types.ts";
210+
import { kFieldCategories } from "../../project/types/website/listing/website-listing-shared.ts";
210211

211212
// in case we are running multiple pandoc processes
212213
// we need to make sure we capture all of the trace files
@@ -1020,6 +1021,10 @@ export async function runPandoc(
10201021
if (key === kTheme && isRevealjsOutput(options.format.pandoc)) {
10211022
continue;
10221023
}
1024+
// - categories are handled specifically already for website projects with a metadata override and should not be overridden by user input
1025+
if (key === kFieldCategories && projectIsWebsite(options.project)) {
1026+
continue;
1027+
}
10231028
// perform the override
10241029
pandocMetadata[key] = engineMetadata[key];
10251030
}

src/core/base64.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export function b64EncodeUnicode(str: string) {
2+
return btoa(encodeURIComponent(str));
3+
}
4+
5+
export function unicodeDecodeB64(str: string) {
6+
return decodeURIComponent(atob(str));
7+
}

src/project/types/website/listing/website-listing-categories.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {
1717
ListingDescriptor,
1818
ListingSharedOptions,
1919
} from "./website-listing-shared.ts";
20+
import { b64EncodeUnicode } from "../../../../core/base64.ts";
2021

2122
export function categorySidebar(
2223
doc: Document,
@@ -117,7 +118,7 @@ function categoryElement(
117118
categoryEl.classList.add("category");
118119
categoryEl.setAttribute(
119120
"data-category",
120-
value !== undefined ? btoa(value) : btoa(category),
121+
value !== undefined ? b64EncodeUnicode(value) : b64EncodeUnicode(category),
121122
);
122123
categoryEl.innerHTML = contents;
123124
return categoryEl;

src/project/types/website/listing/website-listing-template.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ import { formatDate, parsePandocDate } from "../../../../core/date.ts";
4444
import { truncateText } from "../../../../core/text.ts";
4545
import { encodeAttributeValue } from "../../../../core/html.ts";
4646
import { imagePlaceholder, isPlaceHolder } from "./website-listing-read.ts";
47+
import { b64EncodeUnicode, unicodeDecodeB64 } from "../../../../core/base64.ts";
4748

4849
export const kDateFormat = "date-format";
4950

@@ -160,6 +161,12 @@ export function templateMarkdownHandler(
160161
ejsParams["metadataAttrs"] = reshapedListing.utilities.metadataAttrs;
161162
ejsParams["templateParams"] = reshapedListing["template-params"];
162163
}
164+
// some custom utils function
165+
ejsParams["utils"] = {
166+
b64encode: b64EncodeUnicode,
167+
b64decode: unicodeDecodeB64,
168+
};
169+
163170
return ejsParams;
164171
};
165172

@@ -455,7 +462,7 @@ export function reshapeListing(
455462
attr["index"] = (index++).toString();
456463
if (item.categories) {
457464
const str = (item.categories as string[]).join(",");
458-
attr["categories"] = btoa(str);
465+
attr["categories"] = b64EncodeUnicode(str);
459466
}
460467

461468
// Add magic attributes for the sortable values

src/project/types/website/website.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,9 @@ import { formatDate } from "../../../core/date.ts";
8686
import { projectExtensionPathResolver } from "../../../extension/extension.ts";
8787
import { websiteDraftPostProcessor } from "./website-draft.ts";
8888
import { projectDraftMode } from "./website-utils.ts";
89+
import { kFieldCategories } from "./listing/website-listing-shared.ts";
90+
import { pandocNativeStr } from "../../../core/pandoc/codegen.ts";
91+
import { asArray } from "../../../core/array.ts";
8992

9093
export const kSiteTemplateDefault = "default";
9194
export const kSiteTemplateBlog = "blog";
@@ -157,6 +160,7 @@ export const websiteProjectType: ProjectType = {
157160
// add some title related variables
158161
extras.pandoc = extras.pandoc || {};
159162
extras.metadata = extras.metadata || {};
163+
extras.metadataOverride = extras.metadataOverride || {};
160164

161165
// Resolve any giscus information
162166
resolveFormatForGiscus(project, format);
@@ -196,6 +200,18 @@ export const websiteProjectType: ProjectType = {
196200
extras.metadata[kPageTitle] = title;
197201
}
198202

203+
// categories metadata needs to be escaped from Markdown processing to
204+
// avoid +smart applying to it. Categories are expected to be non markdown.
205+
// So we provide an override to ensure they are not processed.
206+
if (format.metadata[kFieldCategories]) {
207+
extras.metadataOverride[kFieldCategories] = asArray(
208+
format.metadata[kFieldCategories],
209+
).map(
210+
(category) =>
211+
pandocNativeStr(category as string).mappedString().value,
212+
);
213+
}
214+
199215
// html metadata
200216
extras.html = extras.html || {};
201217
extras.html[kHtmlPostprocessors] = extras.html[kHtmlPostprocessors] || [];

src/resources/projects/website/listing/item-default.ejs.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ print(`<div class="metadata-value listing-${field}">${listing.utilities.outputLi
5656
<% if (fields.includes('categories') && item.categories) { %>
5757
<div class="listing-categories">
5858
<% for (const category of item.categories) { %>
59-
<div class="listing-category" onclick="window.quartoListingCategory('<%=btoa(category)%>'); return false;"><%= category %></div>
59+
<div class="listing-category" onclick="window.quartoListingCategory('<%=utils.b64encode(category)%>'); return false;"><%= category %></div>
6060
<% } %>
6161
</div>
6262
<% } %>

src/resources/projects/website/listing/item-grid.ejs.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ return !["title", "image", "image-alt", "date", "author", "subtitle", "descripti
6464

6565
<div class="listing-categories">
6666
<% for (const category of item.categories) { %>
67-
<div class="listing-category" onclick="window.quartoListingCategory('<%=btoa(category)%>'); return false;"><%= category %></div>
67+
<div class="listing-category" onclick="window.quartoListingCategory('<%=utils.b64encode(category)%>'); return false;"><%= category %></div>
6868
<% } %>
6969
</div>
7070

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
:::{.list .quarto-listing-default}
22
<% for (const item of items) { %>
3-
<% partial('item-default.ejs.md', {listing, item }) %>
3+
<% partial('item-default.ejs.md', {listing, item, utils }) %>
44
<% } %>
55
:::

src/resources/projects/website/listing/listing-grid.ejs.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,6 @@ const cols = listing['grid-columns'];
44

55
:::{.list .grid .quarto-listing-cols-<%=cols%>}
66
<% for (const item of items) { %>
7-
<% partial('item-grid.ejs.md', {listing, item }) %>
7+
<% partial('item-grid.ejs.md', {listing, item, utils }) %>
88
<% } %>
99
:::

src/resources/projects/website/listing/quarto-listing.js

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@ window["quarto-listing-loaded"] = () => {
1616
if (hash) {
1717
// If there is a category, switch to that
1818
if (hash.category) {
19-
activateCategory(hash.category);
19+
// category hash are URI encoded so we need to decode it before processing
20+
// so that we can match it with the category element processed in JS
21+
activateCategory(decodeURIComponent(hash.category));
2022
}
2123
// Paginate a specific listing
2224
const listingIds = Object.keys(window["quarto-listings"]);
@@ -59,7 +61,10 @@ window.document.addEventListener("DOMContentLoaded", function (_event) {
5961
);
6062

6163
for (const categoryEl of categoryEls) {
62-
const category = atob(categoryEl.getAttribute("data-category"));
64+
// category needs to support non ASCII characters
65+
const category = decodeURIComponent(
66+
atob(categoryEl.getAttribute("data-category"))
67+
);
6368
categoryEl.onclick = () => {
6469
activateCategory(category);
6570
setCategoryHash(category);
@@ -209,7 +214,9 @@ function activateCategory(category) {
209214

210215
// Activate this category
211216
const categoryEl = window.document.querySelector(
212-
`.quarto-listing-category .category[data-category='${btoa(category)}']`
217+
`.quarto-listing-category .category[data-category='${btoa(
218+
encodeURIComponent(category)
219+
)}']`
213220
);
214221
if (categoryEl) {
215222
categoryEl.classList.add("active");
@@ -232,7 +239,9 @@ function filterListingCategory(category) {
232239
list.filter(function (item) {
233240
const itemValues = item.values();
234241
if (itemValues.categories !== null) {
235-
const categories = atob(itemValues.categories).split(",");
242+
const categories = decodeURIComponent(
243+
atob(itemValues.categories)
244+
).split(",");
236245
return categories.includes(category);
237246
} else {
238247
return false;

0 commit comments

Comments
 (0)