Skip to content
Open
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
29 changes: 27 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,12 +1,37 @@
.PHONY: all css serve-dev
.PHONY: all css js serve-dev

define update_cache_buster
hash=$$(sha256sum "$(1)" | cut -c1-8); \
perl -0pi -e "s|(^[[:space:]]*$(2):.*?url: '/)[^']*(')|\$$1$(1)?v=$$hash\$$2|ms" _config.yml
endef

define update_sri
hash=$$(openssl dgst -sha384 -binary "$(1)" | openssl base64 -A); \
perl -0pi -e "s|(^[[:space:]]*$(2):.*?sri: ')[^']*(')|\$$1sha384-$$hash\$$2|ms" _config.yml
endef

all:
@echo '# These are the available make targets'
@grep '^[^#[:space:]\.].*:' Makefile | grep -v all: | tr -d :

css:
npm install
npx less --clean-css="--s1 --advanced" styles/styles.less > styles/styles.min.css
npx lessc --clean-css="--s1 --advanced" styles/styles.less > styles/styles.min.css
$(call update_cache_buster,styles/styles.min.css,css)
$(call update_sri,styles/styles.min.css,css)

js:
npm install
@for file in js/*.js; do \
case "$$file" in \
*.min.js) continue ;; \
esac; \
minified="$${file%.js}.min.js"; \
name="$$(basename "$$file" .js)"; \
npx uglifyjs "$$file" -c -m -o "$$minified"; \
$(call update_cache_buster,$$minified,$$name); \
$(call update_sri,$$minified,$$name); \
done

serve-dev:
docker run -it -p 4000:4000 -v $(PWD):/site -w /site library/ruby:3 bash -c 'bundle install && bundle exec jekyll serve -H 0.0.0.0'
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,7 @@ The pages are rendered with Jekyll and kindly hosted by github.
## Build CSS

To build the stylesheets run `make css`.

## Build JS

To build the stylesheets run `make js`.
228 changes: 228 additions & 0 deletions _bin/update-jsdelivr-refs.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,228 @@
#!/usr/bin/env php
<?php

/**
* Resolve CDN dependencies using the jsDelivr API.
*/
define('BASE_PATH', realpath(__DIR__ . '/..') . '/');
define('JSDELIVR_API', 'https://data.jsdelivr.com/v1');
define('JSDELIVR_CDN', 'https://cdn.jsdelivr.net/npm');

$cfgfile = BASE_PATH . 'jsdelivr.json';
$refs = json_decode(file_get_contents($cfgfile), true);

/**
* Make an HTTP GET request and decode the JSON response.
*/
function apiRequest($url)
{
$ch = curl_init($url);

if ($ch === false) {
throw new RuntimeException('Unable to initialise cURL');
}

curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_TIMEOUT => 15,
CURLOPT_HTTPHEADER => [
'Accept: application/json',
],
]);

$response = curl_exec($ch);

if ($response === false) {
$error = curl_error($ch);
curl_close($ch);

throw new RuntimeException(
sprintf('Request failed: %s', $error)
);
}

$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);

curl_close($ch);

if ($status < 200 || $status >= 300) {
throw new RuntimeException(
sprintf(
'Request failed with HTTP %d: %s',
$status,
$url
)
);
}

$data = json_decode($response, true);

if (!is_array($data)) {
throw new RuntimeException(
sprintf(
'Invalid JSON returned by %s',
$url
)
);
}

return $data;
}

/**
* Resolve a version constraint to a concrete version.
*
* jsDelivr handles the version/semver resolution.
*/
function resolveVersion($package, $constraint)
{
$url = JSDELIVR_API . '/packages/npm/' . rawurlencode($package) . '/resolved?specifier=' . rawurlencode($constraint);
$data = apiRequest($url);

if (!isset($data['version']) || !is_string($data['version'])) {
throw new RuntimeException(
sprintf(
'Unable to resolve version "%s" for %s',
$constraint,
$package
)
);
}

return $data['version'];
}

/**
* Get the package metadata and file listing for a
* specific version.
*/
function getPackageFiles($package, $version)
{
$url = JSDELIVR_API . '/packages/npm/' . rawurlencode($package) . '@' . rawurlencode($version);
$data = apiRequest($url);

if (!isset($data['files']) || !is_array($data['files'])) {
throw new RuntimeException(
sprintf(
'No file listing returned for %s@%s',
$package,
$version
)
);
}

return $data['files'];
}

/**
* Flatten the jsDelivr file tree into a path => metadata array.
*/
function flattenFiles($files, $prefix = '')
{
$result = [];

foreach ($files as $file) {
if (!isset($file['name'])) {
continue;
}

$name = $file['name'];
$path = $prefix . '/' . ltrim($name, '/');

if (($file['type'] ?? null) === 'directory') {
if (isset($file['files']) && is_array($file['files'])) {
$result += flattenFiles($file['files'], $path);
}

continue;
}

$result[$path] = $file;
}

return $result;
}

/**
* Process all dependencies and return the resolved
* CDN configuration.
*/
function processDependencies($refs)
{
$result = [];

foreach ($refs as $name => $dependency) {
$package = $dependency['package'];

if (!isset($dependency['version'])) {
throw new RuntimeException(sprintf('No version specified for %s', $package));
}

$requestedVersion = $dependency['version'];
$resolvedVersion = resolveVersion($package, $requestedVersion);
$files = getPackageFiles($package, $resolvedVersion);
$availableFiles = flattenFiles($files);

foreach (['css', 'js'] as $type) {
if (empty($dependency[$type]) || !is_array($dependency[$type])) {
continue;
}

$result[$name][$type] = [];

foreach ($dependency[$type] as $file) {
if (!isset($availableFiles[$file])) {
throw new RuntimeException(
sprintf(
'File does not exist: %s@%s%s',
$name,
$resolvedVersion,
$file
)
);
}

$fileInfo = $availableFiles[$file];

if (!isset($fileInfo['hash'])|| !is_string($fileInfo['hash'])) {
throw new RuntimeException(
sprintf(
'No hash available for: %s@%s%s',
$name,
$resolvedVersion,
$file
)
);
}

$result[$name][$type][] = [
'url' => JSDELIVR_CDN . '/' . $package . '@' . $resolvedVersion . $file,
'sri' => 'sha256-' . $fileInfo['hash'],
];
}
}
}

return $result;
}


try {
$resolved = processDependencies($refs);
foreach ($resolved as $package => $types) {
echo " {$package}:" . PHP_EOL;

foreach ($types as $type => $files) {
echo " {$type}:" . PHP_EOL;

foreach ($files as $file) {
echo " url: '{$file['url']}'" . PHP_EOL;
echo " sri: '{$file['sri']}'" . PHP_EOL;
}
}
}
} catch (Throwable $e) {
fwrite(STDERR, 'ERROR: ' . $e->getMessage() . PHP_EOL);
exit(1);
}
78 changes: 48 additions & 30 deletions _config.yml
Original file line number Diff line number Diff line change
@@ -1,38 +1,56 @@
safe: true
encoding: UTF-8
url: https://roundcube.net
enforce_ssl: roundcube.net
hostname: "https://roundcube.net"
permalink: news/:year/:month/:day/:title
exclude: [vendor]
plugins: [jekyll-paginate]
paginate: 10
paginate_path: "/news/page:num/"
environment: "production"
paginate_path: '/news/page:num/'
environment: 'production'
num_related_items: 5
libs: {
fonts: {
roboto: 'https://fonts.googleapis.com/css?family=Roboto&amp;display=swap',
},
less: {
url: 'https://cdn.jsdelivr.net/npm/less@4.6.4/dist/less.min.js',
sri: 'sha384-WR+iiQT+XSq8wDUutASKAXx1pZTJCLRyHfpwqBiL1ji4qd2BsJIPhn8k/s72NkPj'
},
css: '/styles/styles.min.css?v=2026051000',
bootstrap: {
css: {
url: 'https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css',
sri: 'sha384-sRIl4kxILFvY47J16cr9ZwB07vP4J8+LH7qKQnuqkuIAvNWLzeN8tE5YBujZqJLB'
},
js: {
url: 'https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/js/bootstrap.bundle.min.js',
sri: 'sha384-FKyoEForCGlyvwx9Hj09JcYn3nv7wiPVlz7YYwJrWVcXK/BmnVDxM+D2scQbITxI'
}
},
fontawesome: {
css: {
url: 'https://cdn.jsdelivr.net/npm/@fortawesome/fontawesome-free@7.2.0/css/all.min.css',
sri: 'sha384-EXatlQyrOJgDaM9/a74ArMzy7/2bTMSrZj8ID1IPeVmc3GncfCugefCFWSLj8JL/'
},
}
}
exclude:
- Gemfile
- Gemfile.lock
- jsdelivr.json
- node_modules/
- Makefile
- package.json
- package-lock.json
- README.md
- stylelint.config.mjs
libs:
css:
url: '/styles/styles.min.css?v=9bf32423'
sri: 'sha384-yb8/IfPlNNl8SDmdqLJ/CA5xbgQHXyHb2ehtoAamh+bLbMmu/tW+EBvF2BdzkzLv'
js:
banner:
url: '/js/banner.min.js?v=f44c2f56'
sri: 'sha384-zF0W4VZ06nGXgt2nFDiPoMhn5+UK+WaAIX1cprNGHkcURqccwGD+fXRZiu77TZ+b'
copy:
url: '/js/copy.min.js?v=ebb9b310'
sri: 'sha384-9TlbtUpnP3OoI5QMwNcfEMbkzjHOvlpeDEjVJCPWHJPxwCA5K2dXqlKZrb07rYVa'
theme:
url: '/js/theme.min.js?v=17d30fbf'
sri: 'sha384-VhNYv2z6f3ZriTjr7a9xelQKHZilrphb/xj36CtPT1McEdLSIPrMG4L+IJkgC1+T'
viewer:
url: '/js/viewer.min.js?v=df6dc21d'
sri: 'sha384-Gn/Zio1YIE6O7P0MXgTN8GECQMnPDd8MYeXQbJV5hk4+6O6aWhkBHcFUFs+DpNDL'
less:
js:
url: 'https://cdn.jsdelivr.net/npm/less@4.9.1/dist/less.min.js'
sri: 'sha256-QoOydTeDccOLjcHMHS1oOUe0QaqHXh2XFuki4s/zSXc='
roboto:
css:
url: 'https://cdn.jsdelivr.net/npm/@fontsource/roboto@5.3.0/400.css'
sri: 'sha256-aYPEXwxOBquazL7uNzcM57whskXEvU0X84EZ2lI41yk='
bootstrap:
css:
url: 'https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css'
sri: 'sha256-2FMn2Zx6PuH5tdBQDRNwrOo60ts5wWPC9R8jK67b3t4='
js:
url: 'https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/js/bootstrap.bundle.min.js'
sri: 'sha256-5P1JGBOIxI7FBAvT/mb1fCnI5n/NhQKzNUuW7Hq0fMc='
fontawesome:
css:
url: 'https://cdn.jsdelivr.net/npm/@fortawesome/fontawesome-free@7.3.1/css/all.min.css'
sri: 'sha256-4Lad8m4ZWW1Lgb9+sMVLYEfnIh7BjV1NQMEe79Pviks='
Loading