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
6 changes: 6 additions & 0 deletions _extensions/glossary/_extension.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
title: Glossary
author: VU Amsterdam
version: 1.0.0
contributes:
shortcodes:
- glossary.lua
97 changes: 97 additions & 0 deletions _extensions/glossary/glossary.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
-- glossary.lua – `{{< glossary term-id >}}` shortcode
--
-- Renders a link to /glossary/<term-id>.html with the term's title and
-- description attached as data attributes, which js/glossary-popup.js uses
-- to show a hover/click popup. Term pages live in glossary/<id>.qmd: the
-- page title is the term, an optional `aliases` metadata field lists
-- alternative spellings, and the page body is the description.
--
-- Usage: For logging in {{< glossary mfa >}} is mandatory.
--
-- Peter Vos, 2026-06-16

local stringify = pandoc.utils.stringify

--- Resolve the glossary/ directory relative to the project root so the
--- shortcode works the same regardless of which subdirectory the current
--- file lives in.
local function glossary_dir()
if quarto.project.directory then
return quarto.project.directory .. '/glossary'
end
return 'glossary'
end

--- Read every glossary/*.qmd file once and cache id -> { id, title, description }.
local terms = nil

local function load_glossary()
local index = {}
local dir = glossary_dir()
local ok, files = pcall(pandoc.system.list_directory, dir)
if not ok or not files then
return index
end

for _, file in ipairs(files) do
local id = file:match '^(.*)%.qmd$' or file:match '^(.*)%.md$'
if id then
local fh = io.open(dir .. '/' .. file, 'r')
if fh then
local text = fh:read '*a'
fh:close()

local doc = pandoc.read(text, 'markdown')
local title = doc.meta.title and stringify(doc.meta.title) or id

local parts = {}
for _, block in ipairs(doc.blocks) do
local part = stringify(block)
if part ~= '' then
table.insert(parts, part)
end
end

local term = { id = id, title = title, description = table.concat(parts, ' ') }
index[id:lower()] = term

if doc.meta.aliases then
for _, alias in ipairs(doc.meta.aliases) do
local alias_id = stringify(alias):lower()
if not index[alias_id] then
index[alias_id] = term
end
end
end
end
end
end

return index
end

return {
['glossary'] = function(args, kwargs, meta)
if terms == nil then
terms = load_glossary()
end

if #args == 0 then
return quarto.shortcode.error_output('glossary', 'missing term id', 'inline')
end

local id = stringify(args[1]):lower()
local term = terms[id]
if not term then
quarto.log.warning("glossary shortcode: unknown term '" .. id .. "'")
return quarto.shortcode.error_output('glossary', "unknown term '" .. id .. "'", 'inline')
end

local link = pandoc.Link({ pandoc.Str(term.title) }, '/glossary/' .. term.id .. '.html')
link.classes = { 'glossary-link' }
link.attributes['data-glossary-title'] = term.title
link.attributes['data-glossary-description'] = term.description
link.attributes['tabindex'] = '0'
return link
end
}
3 changes: 3 additions & 0 deletions _quarto.yml
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ language:

resources:
- "public/*"
- "js/*"
- CNAME
format:
html:
Expand All @@ -72,6 +73,8 @@ format:
<div class="handbook-title">Research Support Handbook</div>
include-in-header:
- text: <script defer src="https://umami.labs.vu.nl/script.js" data-website-id="d8c5a441-b2ab-4d33-ba3a-6647fd821974"></script>
include-after-body:
- text: <script defer src="/js/glossary-popup.js"></script>
# from: "markdown+emoji"
filters:
- utils/include-files.lua
Expand Down
39 changes: 38 additions & 1 deletion custom.scss
Original file line number Diff line number Diff line change
Expand Up @@ -277,7 +277,44 @@ footer.footer .nav-footer {
.listing-image {
display: flex;
align-items: center;
justify-content: center;
justify-content: center;
}
}
/* tools, topics and guides listing and accent color */

/* glossary links and popup */
a.glossary-link {
text-decoration: underline dotted;
text-decoration-color: #0282c9;
text-underline-offset: 2px;
cursor: help;
}

.glossary-tooltip {
position: absolute;
z-index: 1060;
max-width: 320px;
background-color: #1c1c1c;
color: white;
border-radius: .25rem;
padding: .6em .8em;
font-size: .85rem;
line-height: 1.4;
box-shadow: 0 .25rem .75rem rgba(0, 0, 0, .25);
visibility: hidden;
opacity: 0;
transition: opacity .1s ease-in-out;
pointer-events: none;
}

.glossary-tooltip.visible {
visibility: visible;
opacity: 1;
}

.glossary-tooltip-title {
display: block;
margin-bottom: .25em;
color: #6fc2f4;
}
/* glossary links and popup */
5 changes: 5 additions & 0 deletions glossary/mfa.qmd
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
title: "MFA"
aliases: ["Multi-Factor Authentication"]
---
A security method that requires users to verify their identitiy using two or more factors, such as an authentication app or security key. Many tools, including Yoda, Research Drive, GitHub, and Microsoft apps, require MFA for secure access to their virtual environments. Instructions for settting up MFA can be found on the [VU Service Portal](https://vu.service-now.com/kb?id=kb_article_view&sysparm_article=KB0012310).
20 changes: 20 additions & 0 deletions glossary/persistent-identifier.qmd
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
---
title: Persistent Identifier
aliases: ["PID"]
---
In short, and in the current context, a Persistent Identifier (PID) is essentially a URL that will never break. There are multiple PID systems, each with its own particular properties. Examples of widely used PIDs in the research domain include:


### DOI:

A Digital Object Identifier can be used to refer to research data and research software. DOIs can be assigned to datasets and software upon their deposit in a repository.

### ORCiD:

An Open Researcher and Contributor ID is used to create a researcher profile with a unique identification number. Researchers can request an ORCiD themselves, with which they can identify their research output as their work.

### ROR:

The Research Organization Registry is a global register with persistent identifiers for research institutes. Researchers can use the ROR for VU Amsterdam when filling metadata forms for their research output to show that their work has been created within their employment at VU Amsterdam.

See the Persistent Identifier guide of Netwerk Digitaal Erfgoed for a more elaborate overview. Apart from widely used domain-agnostic PIDs, there is a wide range of domain-specific unique identifiers that can be used.
5 changes: 5 additions & 0 deletions glossary/rsm.qmd
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
title: Research Software Management
aliases: ["RSM"]
---
Research software management (RSM) is a structured and strategic approach to handling the creation, utilisation, and preservation of software in the research process.
4 changes: 4 additions & 0 deletions glossary/surf.qmd
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
---
title: SURF
---
SURF is the collaborative organisation for ICT in Dutch education and research. SURF provides a wide range of services, including high-speed internet, cloud services, and data storage solutions, to support the needs of the Dutch research community. VU Amsterdam is a member of SURF, which allows researchers at VU to access these services and benefit from the collaborative efforts of the Dutch research community.
2 changes: 1 addition & 1 deletion guides/onboarding.qmd
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ See [Data Collection](../topics/data-collection.qmd)
#### Security Measures

- Determine the necessary security measures given the [**privacy risk**](https://vu.nl/en/research/dataclassification) of the data.
- Does the recipient need MFA? Full-disk encryption?
- Does the recipient need {{< glossary mfa >}}? Full-disk encryption?

*Contact your [faculty privacy champion](https://vu.nl/en/employee/information-security/privacy-champions-information) before drawing up any contracts*

Expand Down
2 changes: 1 addition & 1 deletion guides/process-and-analyse.qmd
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ Roughly speaking, you should try to get access to the HPC when you need to stick

#### SURF Snellius Compute Cluster

Snellius is the Dutch National supercomputer hosted at SURF. The system facilitates scientific research carried out in many Universities, independent research institutes, governmental organizations, and private companies in the Netherlands.
Snellius is the Dutch National supercomputer hosted at {{< glossary surf >}}. The system facilitates scientific research carried out in many Universities, independent research institutes, governmental organizations, and private companies in the Netherlands.

It’s a service comprising a wide range of resources, compilers and, such as R statistics and MATLAB, and libraries. SURF continually adjusts the service to the needs of the user community. For example, Snellius Compute Cluster includes accelerators (very fast processors),high memory nodes and GPU nodes. <!--# might need a further rewrite -->

Expand Down
108 changes: 108 additions & 0 deletions js/glossary-popup.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
// glossary-popup.js – show a glossary term's definition in a small popup
// when a reader hovers, focuses, or clicks a `.glossary-link` (created by
// utils/glossary.lua from `{{ glossary term-id }}` shortcodes).
//
// Clicking/tapping a link toggles a "pinned" popup instead of navigating,
// so touch users (who have no hover) can still read the definition; the
// link still has a real href to the glossary page for anyone who wants the
// full entry (e.g. ctrl/cmd/middle-click, or a screen reader following it).

document.addEventListener('DOMContentLoaded', function () {
const tooltip = document.createElement('div');
tooltip.className = 'glossary-tooltip';
tooltip.setAttribute('role', 'tooltip');

const title = document.createElement('strong');
title.className = 'glossary-tooltip-title';
const description = document.createElement('div');
description.className = 'glossary-tooltip-description';
tooltip.append(title, description);
document.body.appendChild(tooltip);

let activeLink = null;
let pinned = false;

function positionTooltip(link) {
const rect = link.getBoundingClientRect();
const margin = 10;
const tooltipRect = tooltip.getBoundingClientRect();
let top = window.scrollY + rect.bottom + margin;
let left = window.scrollX + rect.left;

if (left + tooltipRect.width > window.scrollX + window.innerWidth - margin) {
left = window.scrollX + window.innerWidth - tooltipRect.width - margin;
}
if (top + tooltipRect.height > window.scrollY + window.innerHeight - margin) {
top = window.scrollY + rect.top - tooltipRect.height - margin;
}
top = Math.max(top, window.scrollY + margin);
left = Math.max(left, window.scrollX + margin);

tooltip.style.top = top + 'px';
tooltip.style.left = left + 'px';
}

function showTooltip(link) {
if (!link) return;
activeLink = link;
title.textContent = link.dataset.glossaryTitle || link.textContent;
description.textContent = link.dataset.glossaryDescription || '';
tooltip.classList.add('visible');
positionTooltip(link);
}

function hideTooltip() {
tooltip.classList.remove('visible');
activeLink = null;
pinned = false;
}

function isGlossaryLink(el) {
return el && el.closest && el.closest('a.glossary-link');
}

document.querySelectorAll('a.glossary-link').forEach(function (link) {
link.addEventListener('mouseenter', function () {
if (!pinned) showTooltip(link);
});
link.addEventListener('focus', function () {
showTooltip(link);
});
link.addEventListener('mouseleave', function () {
if (!pinned) hideTooltip();
});
link.addEventListener('blur', function () {
if (!pinned) hideTooltip();
});
link.addEventListener('click', function (event) {
if (event.ctrlKey || event.metaKey || event.shiftKey || event.altKey) {
return; // let the browser handle "open in new tab" etc. normally
}
event.preventDefault();
if (pinned && activeLink === link) {
hideTooltip();
} else {
pinned = true;
showTooltip(link);
}
});
});

document.addEventListener('click', function (event) {
if (!isGlossaryLink(event.target)) {
hideTooltip();
}
});

document.addEventListener('keydown', function (event) {
if (event.key === 'Escape') {
hideTooltip();
}
});

window.addEventListener('scroll', function () {
if (activeLink && tooltip.classList.contains('visible')) {
positionTooltip(activeLink);
}
});
});