Skip to content
Merged
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
1 change: 1 addition & 0 deletions .deepsource.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
version = 1
exclude_patterns = [
"_assets/js/vendor/**",
"pnpm-lock.yaml",
]

Expand Down
271 changes: 271 additions & 0 deletions _assets/js/vendor/count.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,271 @@
// GoatCounter: https://www.goatcounter.com
// This file is released under the ISC license: https://opensource.org/licenses/ISC
;(function() {
'use strict';

if (window.goatcounter && window.goatcounter.vars) // Compatibility with very old version; do not use.
window.goatcounter = window.goatcounter.vars
else
window.goatcounter = window.goatcounter || {}

// Load settings from data-goatcounter-settings.
var s = document.querySelector('script[data-goatcounter]')
if (s && s.dataset.goatcounterSettings) {
try { var set = JSON.parse(s.dataset.goatcounterSettings) }
catch (err) { console.error('invalid JSON in data-goatcounter-settings: ' + err) }
for (var k in set)
if (['no_onload', 'no_events', 'allow_local', 'allow_frame', 'path', 'title', 'referrer', 'event'].indexOf(k) > -1)
window.goatcounter[k] = set[k]
}

var enc = encodeURIComponent

// Get all data we're going to send off to the counter endpoint.
var get_data = function(vars) {
var data = {
p: (vars.path === undefined ? goatcounter.path : vars.path),
r: (vars.referrer === undefined ? goatcounter.referrer : vars.referrer),
t: (vars.title === undefined ? goatcounter.title : vars.title),
e: !!(vars.event || goatcounter.event),
s: [window.screen.width, window.screen.height, (window.devicePixelRatio || 1)],
b: is_bot(),
q: location.search,
}

var rcb, pcb, tcb // Save callbacks to apply later.
if (typeof(data.r) === 'function') rcb = data.r
if (typeof(data.t) === 'function') tcb = data.t
if (typeof(data.p) === 'function') pcb = data.p

if (is_empty(data.r)) data.r = document.referrer
if (is_empty(data.t)) data.t = document.title
if (is_empty(data.p)) data.p = get_path()

if (rcb) data.r = rcb(data.r)
if (tcb) data.t = tcb(data.t)
if (pcb) data.p = pcb(data.p)
return data
}

// Check if a value is "empty" for the purpose of get_data().
var is_empty = function(v) { return v === null || v === undefined || typeof(v) === 'function' }

// See if this looks like a bot; there is some additional filtering on the
// backend, but these properties can't be fetched from there.
var is_bot = function() {
// Headless browsers are probably a bot.
var w = window, d = document
if (w.callPhantom || w._phantom || w.phantom)
return 150
if (w.__nightmare)
return 151
if (d.__selenium_unwrapped || d.__webdriver_evaluate || d.__driver_evaluate)
return 152
if (navigator.webdriver)
return 153
return 0
}

// Object to urlencoded string, starting with a ?.
var urlencode = function(obj) {
var p = []
for (var k in obj)
if (obj[k] !== '' && obj[k] !== null && obj[k] !== undefined && obj[k] !== false)
p.push(enc(k) + '=' + enc(obj[k]))
return '?' + p.join('&')
}

// Show a warning in the console.
var warn = function(msg) {
if (console && 'warn' in console)
console.warn('goatcounter: ' + msg)
}

// Get the endpoint to send requests to.
var get_endpoint = function() {
var s = document.querySelector('script[data-goatcounter]')
if (s && s.dataset.goatcounter)
return s.dataset.goatcounter
return (goatcounter.endpoint || window.counter) // counter is for compat; don't use.
}

// Get current path.
var get_path = function() {
var loc = location,
c = document.querySelector('link[rel="canonical"][href]')
if (c) { // May be relative or point to different domain.
var a = document.createElement('a')
a.href = c.href
if (a.hostname.replace(/^www\./, '') === location.hostname.replace(/^www\./, ''))
loc = a
}
return (loc.pathname + loc.search) || '/'
}

// Run function after DOM is loaded.
var on_load = function(f) {
if (document.body === null)
document.addEventListener('DOMContentLoaded', function() { f() }, false)
else
f()
}

// Filter some requests that we (probably) don't want to count.
goatcounter.filter = function() {
if ('visibilityState' in document && document.visibilityState === 'prerender')
return 'visibilityState'
if (!goatcounter.allow_frame && location !== parent.location)
return 'frame'
if (!goatcounter.allow_local && location.hostname.match(/(localhost$|^127\.|^10\.|^172\.(1[6-9]|2[0-9]|3[0-1])\.|^192\.168\.|^0\.0\.0\.0$)/))
return 'localhost'
if (!goatcounter.allow_local && location.protocol === 'file:')
return 'localfile'
if (localStorage && localStorage.getItem('skipgc') === 't')
return 'disabled with #toggle-goatcounter'
return false
}

// Get URL to send to GoatCounter.
window.goatcounter.url = function(vars) {
var data = get_data(vars || {})
if (data.p === null) // null from user callback.
return
data.rnd = Math.random().toString(36).substr(2, 5) // Browsers don't always listen to Cache-Control.

var endpoint = get_endpoint()
if (!endpoint)
return warn('no endpoint found')

return endpoint + urlencode(data)
}

// Count a hit.
window.goatcounter.count = function(vars) {
var f = goatcounter.filter()
if (f)
return warn('not counting because of: ' + f)
var url = goatcounter.url(vars)
if (!url)
return warn('not counting because path callback returned null')

if (!navigator.sendBeacon(url)) {
// This mostly fails due to being blocked by CSP; try again with an
// image-based fallback.
var img = document.createElement('img')
img.src = url
img.style.position = 'absolute' // Affect layout less.
img.style.bottom = '0px'
img.style.width = '1px'
img.style.height = '1px'
img.loading = 'eager'
img.setAttribute('alt', '')
img.setAttribute('aria-hidden', 'true')

var rm = function() { if (img && img.parentNode) img.parentNode.removeChild(img) }
img.addEventListener('load', rm, false)
document.body.appendChild(img)
}
}

// Get a query parameter.
window.goatcounter.get_query = function(name) {
var s = location.search.substr(1).split('&')
for (var i = 0; i < s.length; i++)
if (s[i].toLowerCase().indexOf(name.toLowerCase() + '=') === 0)
return s[i].substr(name.length + 1)
}

// Track click events.
window.goatcounter.bind_events = function() {
if (!document.querySelectorAll) // Just in case someone uses an ancient browser.
return

var send = function(elem) {
return function() {
goatcounter.count({
event: true,
path: (elem.dataset.goatcounterClick || elem.name || elem.id || ''),
title: (elem.dataset.goatcounterTitle || elem.title || (elem.innerHTML || '').substr(0, 200) || ''),
referrer: (elem.dataset.goatcounterReferrer || elem.dataset.goatcounterReferral || ''),
})
}
}

Array.prototype.slice.call(document.querySelectorAll("*[data-goatcounter-click]")).forEach(function(elem) {
if (elem.dataset.goatcounterBound)
return
var f = send(elem)
elem.addEventListener('click', f, false)
elem.addEventListener('auxclick', f, false) // Middle click.
elem.dataset.goatcounterBound = 'true'
})
}

// Add a "visitor counter" frame or image.
window.goatcounter.visit_count = function(opt) {
on_load(function() {
opt = opt || {}
opt.type = opt.type || 'html'
opt.append = opt.append || 'body'
opt.path = opt.path || get_path()
opt.attr = opt.attr || {width: '200', height: (opt.no_branding ? '60' : '80')}

opt.attr['src'] = get_endpoint() + 'er/' + enc(opt.path) + '.' + enc(opt.type) + '?'
if (opt.no_branding) opt.attr['src'] += '&no_branding=1'
if (opt.style) opt.attr['src'] += '&style=' + enc(opt.style)
if (opt.start) opt.attr['src'] += '&start=' + enc(opt.start)
if (opt.end) opt.attr['src'] += '&end=' + enc(opt.end)

var tag = {png: 'img', svg: 'img', html: 'iframe'}[opt.type]
if (!tag)
return warn('visit_count: unknown type: ' + opt.type)

if (opt.type === 'html') {
opt.attr['frameborder'] = '0'
opt.attr['scrolling'] = 'no'
}

var d = document.createElement(tag)
for (var k in opt.attr)
d.setAttribute(k, opt.attr[k])

var p = document.querySelector(opt.append)
if (!p)
return warn('visit_count: append not found: ' + opt.append)
p.appendChild(d)
})
}

// Make it easy to skip your own views.
if (location.hash === '#toggle-goatcounter') {
if (localStorage.getItem('skipgc') === 't') {
localStorage.removeItem('skipgc', 't')
alert('GoatCounter tracking is now ENABLED in this browser.')
}
else {
localStorage.setItem('skipgc', 't')
alert('GoatCounter tracking is now DISABLED in this browser until ' + location + ' is loaded again.')
}
}

if (!goatcounter.no_onload)
on_load(function() {
// 1. Page is visible, count request.
// 2. Page is not yet visible; wait until it switches to 'visible' and count.
// See #487
if (!('visibilityState' in document) || document.visibilityState === 'visible')
goatcounter.count()
else {
var f = function(e) {
if (document.visibilityState !== 'visible')
return
document.removeEventListener('visibilitychange', f)
goatcounter.count()
}
document.addEventListener('visibilitychange', f)
}

if (!goatcounter.no_events)
goatcounter.bind_events()
})
})();
11 changes: 11 additions & 0 deletions _includes/head.liquid
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,17 @@
{{- title }} &middot; {{ siteTitle -}}
{%- endif %}
</title>
{% if env == 'production' -%}
<script>
if (window.location.host !== 'open.inf.is')
window.goatcounter = { no_onload: true };
</script>
<script
data-goatcounter='https://openinf.goatcounter.com/count'
async
src='/assets/js/vendor/count.js'
></script>
{%- endif -%}
<script
type='module'
src='https://cdn.ampproject.org/bento.mjs'
Expand Down
19 changes: 11 additions & 8 deletions biome.jsonc
Original file line number Diff line number Diff line change
@@ -1,20 +1,23 @@
{
"$schema": "./node_modules/@biomejs/biome/configuration_schema.json",
"files": {
"ignore": [
"**/_site/",
"**/.git/",
"**/node_modules/",
"**/package.json",
"**/pnpm-lock.yaml",
"assets/js/vendor/"
]
},
"formatter": {
"enabled": true,
"formatWithErrors": false,
"indentStyle": "space",
"indentWidth": 2,
"lineEnding": "lf",
"lineWidth": 80,
"attributePosition": "auto",
"ignore": [
"**/.git/",
"**/_site/",
"**/node_modules/",
"**/package.json",
"**/pnpm-lock.yaml"
]
"attributePosition": "auto"
},
"organizeImports": { "enabled": true },
"linter": {
Expand Down
10 changes: 10 additions & 0 deletions build/tasks/compile/build-js.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/**
* @file Build portal JS.
* @author The OpenINF Authors & Friends
* @license MIT OR Apache-2.0 OR BlueOak-1.0.0
* @module {type ES6Module} build/tasks/compile/build-js
*/

import { jsify } from '@openinf/portal/build/tasks/jsify';

jsify();
1 change: 1 addition & 0 deletions build/tasks/compile/build-portal.mts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ process.env.ELEVENTY_ENV = 'production';
const scripts = [
'nps compile.buildStyles',
'nps compile.buildImages',
'nps compile.buildJs',
'eleventy',
];

Expand Down
35 changes: 2 additions & 33 deletions build/tasks/compile/imagize.mts
Original file line number Diff line number Diff line change
Expand Up @@ -9,43 +9,12 @@
// Requirements
// -----------------------------------------------------------------------------

import { copyFile, mkdir } from 'node:fs/promises';
import {
dirname as pathDirname,
join as pathJoin,
relative as pathRelative,
resolve as pathResolve,
} from 'node:path';
import { resolve as pathResolve } from 'node:path';
import { PATHS } from '@openinf/portal/build/constants';
import { glob } from '@openinf/portal/build/utils';
import { copyFileWithDirStructure, glob } from '@openinf/portal/build/utils';

const imageFiles = await glob(PATHS.imageFilesGlob);

// -----------------------------------------------------------------------------
// Helpers
// -----------------------------------------------------------------------------

/**
* Copies a file while preserving its directory structure.
* @param {string} source The source file path.
* @param {string} sourceBaseDir The base directory to remove from the source path to get the relative structure.
* @param {string} targetBaseDir The base target directory where the relative path will be created.
*/
async function copyFileWithDirStructure(source, sourceBaseDir, targetBaseDir) {
// Determine the relative path from the source base directory.
const relativePath = pathRelative(sourceBaseDir, source);

// Determine the full target path based on the relative path.
const target = pathJoin(targetBaseDir, relativePath);

// Ensure the target directory exists.
const targetDir = pathDirname(target);
await mkdir(targetDir, { recursive: true });

// Copy the file.
await copyFile(source, target);
}

// -----------------------------------------------------------------------------
// Task
// -----------------------------------------------------------------------------
Expand Down
Loading