Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 20 additions & 1 deletion CONTRIBUTING.qmd
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,15 @@ See [Quarto guide](./software/quarto/index.qmd) for more details on how to use Q
* Each directory should have an `index.qmd` file
* Use consistent naming conventions for files: lowercase, hyphenated names such as `data-collection.qmd`

### Registering a New Page in the Navigation

The site uses one sidebar per topic section, defined in `_quarto.yml` under `website: sidebar:`. When you add a page:

1. Add the page to its section's sidebar entry in `_quarto.yml` (find the sidebar with the matching `id:`, such as `data-quality`, and add a `text:`/`href:` pair in the right place).
2. Set the page's `categories:` field to its Diataxis type (`tutorial`, `how-to`, `reference`, or `explanation`). Pages categorized `how-to` appear automatically in the [How-to Guides hub](how-to-guides.qmd).

Section landing pages (`index.qmd`) follow a shared style: a short introduction, a "What You Will Find Here" section using `::: {.callout-tip appearance="simple"}` blocks that group links to child pages, a "Getting Started" pointer, and related resources. See `data-quality/index.qmd` or `monitoring-evaluation-learning/index.qmd` for examples.

### Quarto Basics

* Content uses Markdown with Quarto extensions
Expand All @@ -171,7 +180,17 @@ Example YAML frontmatter:
````yaml
---
title: "My New Page"
description: "Description of the page content"
abstract: "Description of the page content"

#------------------------------------------------------------------
# Diataxis type
#------------------------------------------------------------------
# Exactly one of: tutorial | how-to | reference | explanation
# Rendered as a badge above the page title and used by listing
# pages such as the How-to Guides hub.
#------------------------------------------------------------------
categories:
- explanation


#------------------------------------------------------------------
Expand Down
7 changes: 7 additions & 0 deletions _extensions/diataxis-badge/_extension.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
title: Diataxis Badge
author: IPA Research and Data Science Hub
version: 1.0.0
quarto-required: ">=1.2.0"
contributes:
filters:
- diataxis-badge.lua
50 changes: 50 additions & 0 deletions _extensions/diataxis-badge/diataxis-badge.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
--[[
Quarto filter that reads the page's `categories` metadata and, when one of
the four Diataxis documentation types is present, sets two metadata fields
consumed by the title-block template partial
(assets/design-styles/title-block.html):

diataxis-type - slug used in the badge's CSS class
(tutorial | how-to | reference | explanation)
diataxis-label - display text for the badge

Canonical values in `categories` are matched case-insensitively, and a few
common variants (e.g. "how-to guide") are normalized.
]]--

local LABELS = {
["tutorial"] = { slug = "tutorial", label = "Tutorial" },
["tutorials"] = { slug = "tutorial", label = "Tutorial" },
["how-to"] = { slug = "how-to", label = "How-to guide" },
["how-to guide"] = { slug = "how-to", label = "How-to guide" },
["how-to guides"] = { slug = "how-to", label = "How-to guide" },
["reference"] = { slug = "reference", label = "Reference" },
["explanation"] = { slug = "explanation", label = "Explanation" },
}

function Meta(meta)
if meta["diataxis-type"] ~= nil then
return meta
end

local categories = meta["categories"]
if categories == nil then
return meta
end

if categories.t ~= "List" then
categories = { categories }
end

for _, item in ipairs(categories) do
local value = pandoc.utils.stringify(item):lower()
local match = LABELS[value]
if match then
meta["diataxis-type"] = pandoc.MetaString(match.slug)
meta["diataxis-label"] = pandoc.MetaString(match.label)
break
end
end

return meta
end
2 changes: 2 additions & 0 deletions _freeze/site_libs/quarto-listing/list.min.js

Large diffs are not rendered by default.

254 changes: 254 additions & 0 deletions _freeze/site_libs/quarto-listing/quarto-listing.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,254 @@
const kProgressiveAttr = "data-src";
let categoriesLoaded = false;

window.quartoListingCategory = (category) => {
// category is URI encoded in EJS template for UTF-8 support
category = decodeURIComponent(atob(category));
if (categoriesLoaded) {
activateCategory(category);
setCategoryHash(category);
}
};

window["quarto-listing-loaded"] = () => {
// Process any existing hash
const hash = getHash();

if (hash) {
// If there is a category, switch to that
if (hash.category) {
// category hash are URI encoded so we need to decode it before processing
// so that we can match it with the category element processed in JS
activateCategory(decodeURIComponent(hash.category));
}
// Paginate a specific listing
const listingIds = Object.keys(window["quarto-listings"]);
for (const listingId of listingIds) {
const page = hash[getListingPageKey(listingId)];
if (page) {
showPage(listingId, page);
}
}
}

const listingIds = Object.keys(window["quarto-listings"]);
for (const listingId of listingIds) {
// The actual list
const list = window["quarto-listings"][listingId];

// Update the handlers for pagination events
refreshPaginationHandlers(listingId);

// Render any visible items that need it
renderVisibleProgressiveImages(list);

// Whenever the list is updated, we also need to
// attach handlers to the new pagination elements
// and refresh any newly visible items.
list.on("updated", function () {
renderVisibleProgressiveImages(list);
setTimeout(() => refreshPaginationHandlers(listingId));

// Show or hide the no matching message
toggleNoMatchingMessage(list);
});
}
};

window.document.addEventListener("DOMContentLoaded", function (_event) {
// Attach click handlers to categories
const categoryEls = window.document.querySelectorAll(
".quarto-listing-category .category"
);

for (const categoryEl of categoryEls) {
// category needs to support non ASCII characters
const category = decodeURIComponent(
atob(categoryEl.getAttribute("data-category"))
);
categoryEl.onclick = () => {
activateCategory(category);
setCategoryHash(category);
};
}

// Attach a click handler to the category title
// (there should be only one, but since it is a class name, handle N)
const categoryTitleEls = window.document.querySelectorAll(
".quarto-listing-category-title"
);
for (const categoryTitleEl of categoryTitleEls) {
categoryTitleEl.onclick = () => {
activateCategory("");
setCategoryHash("");
};
}

categoriesLoaded = true;
});

function toggleNoMatchingMessage(list) {
const selector = `#${list.listContainer.id} .listing-no-matching`;
const noMatchingEl = window.document.querySelector(selector);
if (noMatchingEl) {
if (list.visibleItems.length === 0) {
noMatchingEl.classList.remove("d-none");
} else {
if (!noMatchingEl.classList.contains("d-none")) {
noMatchingEl.classList.add("d-none");
}
}
}
}

function setCategoryHash(category) {
setHash({ category });
}

function setPageHash(listingId, page) {
const currentHash = getHash() || {};
currentHash[getListingPageKey(listingId)] = page;
setHash(currentHash);
}

function getListingPageKey(listingId) {
return `${listingId}-page`;
}

function refreshPaginationHandlers(listingId) {
const listingEl = window.document.getElementById(listingId);
const paginationEls = listingEl.querySelectorAll(
".pagination li.page-item:not(.disabled) .page.page-link"
);
for (const paginationEl of paginationEls) {
paginationEl.onclick = (sender) => {
setPageHash(listingId, sender.target.getAttribute("data-i"));
showPage(listingId, sender.target.getAttribute("data-i"));
return false;
};
}
}

function renderVisibleProgressiveImages(list) {
// Run through the visible items and render any progressive images
for (const item of list.visibleItems) {
const itemEl = item.elm;
if (itemEl) {
const progressiveImgs = itemEl.querySelectorAll(
`img[${kProgressiveAttr}]`
);
for (const progressiveImg of progressiveImgs) {
const srcValue = progressiveImg.getAttribute(kProgressiveAttr);
if (srcValue) {
progressiveImg.setAttribute("src", srcValue);
}
progressiveImg.removeAttribute(kProgressiveAttr);
}
}
}
}

function getHash() {
// Hashes are of the form
// #name:value|name1:value1|name2:value2
const currentUrl = new URL(window.location);
const hashRaw = currentUrl.hash ? currentUrl.hash.slice(1) : undefined;
return parseHash(hashRaw);
}

const kAnd = "&";
const kEquals = "=";

function parseHash(hash) {
if (!hash) {
return undefined;
}
const hasValuesStrs = hash.split(kAnd);
const hashValues = hasValuesStrs
.map((hashValueStr) => {
const vals = hashValueStr.split(kEquals);
if (vals.length === 2) {
return { name: vals[0], value: vals[1] };
} else {
return undefined;
}
})
.filter((value) => {
return value !== undefined;
});

const hashObj = {};
hashValues.forEach((hashValue) => {
hashObj[hashValue.name] = decodeURIComponent(hashValue.value);
});
return hashObj;
}

function makeHash(obj) {
return Object.keys(obj)
.map((key) => {
return `${key}${kEquals}${obj[key]}`;
})
.join(kAnd);
}

function setHash(obj) {
const hash = makeHash(obj);
window.history.pushState(null, null, `#${hash}`);
}

function showPage(listingId, page) {
const list = window["quarto-listings"][listingId];
if (list) {
list.show((page - 1) * list.page + 1, list.page);
}
}

function activateCategory(category) {
// Deactivate existing categories
const activeEls = window.document.querySelectorAll(
".quarto-listing-category .category.active"
);
for (const activeEl of activeEls) {
activeEl.classList.remove("active");
}

// Activate this category
const categoryEl = window.document.querySelector(
`.quarto-listing-category .category[data-category='${btoa(
encodeURIComponent(category)
)}']`
);
if (categoryEl) {
categoryEl.classList.add("active");
}

// Filter the listings to this category
filterListingCategory(category);
}

function filterListingCategory(category) {
const listingIds = Object.keys(window["quarto-listings"]);
for (const listingId of listingIds) {
const list = window["quarto-listings"][listingId];
if (list) {
if (category === "") {
// resets the filter
list.filter();
} else {
// filter to this category
list.filter(function (item) {
const itemValues = item.values();
if (itemValues.categories !== null) {
const categories = decodeURIComponent(
atob(itemValues.categories)
).split(",");
return categories.includes(category);
} else {
return false;
}
});
}
}
}
}
Loading
Loading