Skip to content

Commit 52b8736

Browse files
committed
FAIR Best Practices Guide v2 publish
1 parent a3aadf0 commit 52b8736

File tree

20 files changed

+1490
-141
lines changed

20 files changed

+1490
-141
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,5 @@ pages/public/img/.DS_Store
1010
pages/public/img/platforms/.DS_Store
1111
pages/public/img/terms/.DS_Store
1212
.vscode
13+
pages/Community and Best Practices/FAIR and Open Science Best Practices/comments.md
14+
comments.md
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
<template>
2+
<section class="checklist" :aria-label="title || 'Checklist'">
3+
<header class="checklist__head">
4+
<h3>{{ title || 'Checklist' }}</h3>
5+
<span class="checklist__count">{{ doneCount }}/{{ items.length }}</span>
6+
</header>
7+
8+
<ul class="checklist__list">
9+
<li v-for="(text, i) in items" :key="i" class="checklist__item">
10+
<label class="checklist__row">
11+
<input type="checkbox" v-model="doneMask[i]" />
12+
<!-- Render Markdown-style links and resolve VitePress-relative links -->
13+
<span :class="{ done: doneMask[i] }" v-html="renderItem(text)"></span>
14+
</label>
15+
</li>
16+
</ul>
17+
18+
<footer class="checklist__foot">
19+
<button class="checklist__btn" @click="clearDone" :disabled="doneCount === 0">
20+
Clear completed
21+
</button>
22+
</footer>
23+
</section>
24+
</template>
25+
26+
<script setup>
27+
import { ref, computed, onMounted, watch } from 'vue'
28+
import { useRoute } from 'vitepress' // withBase is handled manually to avoid double-base bugs
29+
import DOMPurify from 'dompurify' // ensure this component is wrapped in <ClientOnly> in MD
30+
31+
const props = defineProps({
32+
items: { type: Array, default: () => [] },
33+
title: { type: String, default: 'Checklist' },
34+
storageKey: { type: String, default: 'vp-checklist' }
35+
})
36+
37+
// Persist completion state by index — not item text.
38+
const doneMask = ref([])
39+
40+
function resizeMask(len) {
41+
const mask = Array.isArray(doneMask.value) ? doneMask.value : []
42+
if (mask.length === len) return
43+
if (mask.length > len) doneMask.value = mask.slice(0, len)
44+
else doneMask.value = mask.concat(Array(len - mask.length).fill(false))
45+
}
46+
47+
function load() {
48+
resizeMask(props.items.length)
49+
try {
50+
const raw = localStorage.getItem(props.storageKey)
51+
if (!raw) return
52+
const arr = JSON.parse(raw)
53+
if (Array.isArray(arr)) {
54+
doneMask.value = arr.map(Boolean)
55+
resizeMask(props.items.length)
56+
}
57+
} catch {}
58+
}
59+
60+
function persist() {
61+
try { localStorage.setItem(props.storageKey, JSON.stringify(doneMask.value)) } catch {}
62+
}
63+
64+
const doneCount = computed(() => doneMask.value.filter(Boolean).length)
65+
66+
function clearDone() {
67+
doneMask.value = doneMask.value.map(() => false)
68+
}
69+
70+
// --- Link resolution helpers -------------------------------------------------
71+
72+
const route = useRoute()
73+
// Vite/VitePress base (e.g., '/documentation/')
74+
const BASE = (import.meta.env.BASE_URL || '/').replace(/\/+$/, '/') // ensure trailing slash
75+
76+
function isExternal(href) {
77+
return /^(?:https?:)?\/\//i.test(href) || href.startsWith('mailto:')
78+
}
79+
80+
// Normalize internal path so base is applied exactly once
81+
function applyBase(pathname) {
82+
// ensure leading slash
83+
let p = pathname.startsWith('/') ? pathname : '/' + pathname.replace(/^\.?\//, '')
84+
// avoid double base (e.g., '/documentation/documentation/...'):
85+
if (BASE !== '/' && p.startsWith(BASE)) return p
86+
// prepend base (strip trailing slash from base when concatenating)
87+
const base = BASE.endsWith('/') ? BASE.slice(0, -1) : BASE
88+
return base + p
89+
}
90+
91+
// Resolve internal/relative links similar to VitePress router behavior.
92+
function resolveInternalLink(href) {
93+
if (!href) return href
94+
if (isExternal(href) || href.startsWith('#')) return href
95+
96+
// Base directory of current page (e.g., '/guide/intro.html' -> '/guide/')
97+
const baseDir = route.path.endsWith('/') ? route.path : route.path.replace(/[^/]+$/, '/')
98+
99+
// Build URL relative to current route (uses window.location only on client)
100+
const origin = typeof window !== 'undefined' ? window.location.origin : 'http://localhost'
101+
const u = new URL(href, origin + baseDir)
102+
103+
let p = u.pathname
104+
105+
// Convert .md to .html for internal docs pages
106+
if (/\.md$/i.test(p)) {
107+
p = p.replace(/\.md$/i, '.html')
108+
}
109+
110+
// If the path looks like a directory (no extension) and doesn’t end with '/', add trailing slash
111+
const hasExt = /\.[a-z0-9]+$/i.test(p)
112+
if (!hasExt && !p.endsWith('/')) {
113+
p = p + '/'
114+
}
115+
116+
// Prepend site base exactly once; preserve query/hash
117+
const resolved = applyBase(p) + (u.search || '') + (u.hash || '')
118+
return resolved
119+
}
120+
121+
// Convert Markdown links [label](url) -> HTML <a> with resolved href.
122+
function mdLinksToHtml(s) {
123+
if (typeof s !== 'string') return ''
124+
return s.replace(/\[([^\]]+)\]\(([^)\s]+)\)/g, (_m, label, url) => {
125+
const href = resolveInternalLink(String(url).trim())
126+
const external = isExternal(href)
127+
const target = external ? ' target="_blank" rel="noopener noreferrer"' : ''
128+
return `<a href="${href}"${target}>${String(label).trim()}</a>`
129+
})
130+
}
131+
132+
function renderItem(text) {
133+
const html = mdLinksToHtml(String(text ?? ''))
134+
// Sanitize to avoid XSS; only <a> with href/target/rel is allowed
135+
return typeof window === 'undefined'
136+
? '' // SSR guard (component should be inside <ClientOnly>)
137+
: DOMPurify.sanitize(html, {
138+
ALLOWED_TAGS: ['a'],
139+
ALLOWED_ATTR: ['href', 'target', 'rel']
140+
})
141+
}
142+
143+
onMounted(load)
144+
watch(() => props.items, (nv) => resizeMask(nv.length))
145+
watch(doneMask, persist, { deep: true })
146+
</script>
147+
148+
<style scoped>
149+
.checklist { display: block; }
150+
.checklist__head { display: flex; justify-content: space-between; align-items: baseline; margin-bottom: 8px; }
151+
.checklist__count { font-variant-numeric: tabular-nums; opacity: 0.7; }
152+
.checklist__list { list-style: none; padding: 0; margin: 0; }
153+
.checklist__item { padding: 6px 0; }
154+
.checklist__row { display: flex; align-items: center; gap: 10px; }
155+
.done { text-decoration: line-through; opacity: 0.6; }
156+
.checklist__foot { display: flex; justify-content: flex-end; margin-top: 8px; }
157+
.checklist__btn {
158+
padding: 6px 12px;
159+
border-radius: 6px;
160+
border: 1px solid var(--vp-c-divider);
161+
background-color: var(--vp-c-bg);
162+
cursor: pointer;
163+
transition: background-color 0.2s ease, opacity 0.2s ease;
164+
}
165+
.checklist__btn:hover:not(:disabled) { background-color: var(--vp-c-bg-soft); }
166+
.checklist__btn:disabled { opacity: 0.4; cursor: not-allowed; }
167+
</style>

.vitepress/theme/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import Layout from "./Layout.vue";
22
import "./style.css";
33
import FeatureCard from './components/FeatureCard.vue'
4+
import Checklist from './components/Checklist.vue'
45

56

67
/** @type {import('vitepress').Theme} */
@@ -12,5 +13,6 @@ export default {
1213
app.use(await import("@eox/esa-ui/components/cookies.js"));
1314
}
1415
app.component('FeatureCard', FeatureCard)
16+
app.component('Checklist', Checklist)
1517
}
1618
};

package-lock.json

Lines changed: 17 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
},
1212
"dependencies": {
1313
"@eox/esa-ui": "^1.11.1",
14+
"dompurify": "^3.2.6",
1415
"mystmd": "^1.3.25"
1516
}
1617
}

0 commit comments

Comments
 (0)