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
70 changes: 70 additions & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
module.exports = {
env: {
browser: true,
es6: true,
},
extends: [
'airbnb-base',
'plugin:unicorn/recommended',
],
parserOptions: {
ecmaVersion: '2022',
sourceType: 'module',
},
ignorePatterns: [
'!.*',
'*.min.js',
],
rules: {
'brace-style': ['error', '1tbs'],
'comma-dangle': ['error', {
arrays: 'always-multiline',
exports: 'always-multiline',
functions: 'never',
imports: 'always-multiline',
objects: 'always-multiline',
}],
curly: ['error', 'all'],
indent: ['error', 4, {
SwitchCase: 1,
}],
'linebreak-style': ['error', 'unix'],
'no-multi-spaces': ['error', {
exceptions: {
Property: true,
VariableDeclarator: true,
},
}],
'object-shorthand': ['error', 'never'],
'padding-line-between-statements': ['error', {
blankLine: 'always',
next: ['continue', 'break', 'export', 'return', 'throw'],
prev: '*',
}],
'spaced-comment': ['error', 'always', {
block: {
balanced: true,
exceptions: ['*'],
markers: ['!'],
},
line: {
exceptions: ['-', '+'],
markers: ['/'],
},
}],
strict: 'off',
'wrap-iife': ['error', 'inside'],
'func-names': 'off',
'no-param-reassign': 'off',
'no-use-before-define': 'off',
'prefer-destructuring': 'off',
'unicorn/prefer-module': 'off',
'unicorn/prefer-query-selector': 'off',
'unicorn/no-array-for-each': 'off',
'unicorn/prevent-abbreviations': 'off',
},
reportUnusedDisableDirectives: true,
globals: {
bootstrap: true,
},
};
45 changes: 45 additions & 0 deletions .github/workflows/lintcheck.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
name: LintCheck

on:
push:
pull_request:

permissions:
contents: read

jobs:
validate:
if: "!contains(github.event.head_commit.message, '[skip ci]') && !contains(github.event.head_commit.message, '[ci skip]') && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name)"
runs-on: ubuntu-latest

steps:
- name: Checkout code
uses: actions/checkout@v6

- name: Setup Node
uses: actions/setup-node@v7
with:
node-version: 22

- name: Install Node dependencies
run: npm install --omit=optional

- name: Lint JavaScript
run: npx eslint --ext .js .

- name: Lint Less
run: npx stylelint "./styles/styles.less"

- name: Setup Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: "3.4"
bundler-cache: true

- name: Build site
run: bundle exec jekyll build

- name: Lint HTML
# Find generated HTML, excluding news pages as then contain HTML4
run: find _site -name '*.html' ! -path '_site/news/*/*.html' -print0 | xargs -0 -r npx htmlhint -c .htmlhintrc.js

26 changes: 26 additions & 0 deletions .htmlhintrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{
"doctype-first": true,
"doctype-html5": true,

"tagname-lowercase": true,
"attr-lowercase": true,
"attr-value-double-quotes": true,
"attr-no-duplication": true,
"attr-value-no-duplication": true,

"tag-pair": true,
"empty-tag-not-self-closed": true,
"tag-no-obsolete": true,

"spec-char-escape": true,
"src-not-empty": true,
"alt-require": true,
"id-unique": true,

"html-lang-require": true,
"title-require": true,

"button-type-require": true,
"input-requires-label": true,
"frame-title-require": true,
}
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`.
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&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
- node_modules/
- Makefile
- package.json
- package-lock.json
- README.md
- stylelint.config.mjs
- vendor
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='
105 changes: 14 additions & 91 deletions _includes/footer.html
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@
<span class="fa-solid fa-palette me-1"></span> Switch theme
</a>
<ul class="dropdown-menu py-1">
<li><a class="dropdown-item px-2 pe-3 py-1 rc-icon light-mode" href="#" onclick="switch_color_mode('light'); return false;">Light theme</a></li>
<li><a class="dropdown-item px-2 pe-3 py-1 rc-icon dark-mode" href="#" onclick="switch_color_mode('dark'); return false;">Dark theme</a></li>
<li><a class="dropdown-item px-2 pe-3 py-1 rc-icon auto-mode current-theme" href="#" onclick="switch_color_mode('auto'); return false;">Auto detect</a></li>
<li><a class="dropdown-item px-2 pe-3 py-1 rc-icon light-mode" href="#" data-rc-color-mode="light">Light theme</a></li>
<li><a class="dropdown-item px-2 pe-3 py-1 rc-icon dark-mode" href="#" data-rc-color-mode="dark">Dark theme</a></li>
<li><a class="dropdown-item px-2 pe-3 py-1 rc-icon auto-mode current-theme" href="#" data-rc-color-mode="auto">Auto detect</a></li>
</ul>
</div>
</li>
Expand All @@ -41,91 +41,14 @@

<script src="{{ site.libs.bootstrap.js.url }}" integrity="{{ site.libs.bootstrap.js.sri }}" crossorigin="anonymous"></script>

<script>
var color_mode = 'auto';
if (typeof(Storage) !== "undefined" && window.localStorage.getItem("color-mode") !== null) {
color_mode = window.localStorage.getItem("color-mode");
}
switch_color_mode(color_mode, true);

function switch_color_mode(mode, onload) {
var color_mode = mode;

if (mode == 'auto') {
color_mode = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
}

document.getElementsByTagName('html')[0].setAttribute('data-bs-theme', color_mode)
document.querySelector('div.color-modes > ul.dropdown-menu a.current-theme').classList.remove('current-theme');
document.querySelector('div.color-modes > ul.dropdown-menu a.' + mode + '-mode').classList.add('current-theme');

if (!onload && typeof(Storage) !== "undefined") {
if (mode == 'auto') {
window.localStorage.removeItem('color-mode');
}
else {
window.localStorage.setItem('color-mode', mode);
}
}

var event = new Event('switch-color-mode');
document.dispatchEvent(event);
}

var copytext = "Copy to clipboard";
var copycomplete = "Copied";
var copyerror = "Copy failed";

document.querySelectorAll('a.copy-popover-link').forEach((sumbtn) => {
var popover = new bootstrap.Popover(sumbtn, {
content: sumbtn.closest('td').querySelector('.copy-popover-target').innerHTML,
html: true,
customClass: 'copy-popover'
});

sumbtn.addEventListener('shown.bs.popover', () => {
document.querySelectorAll('div.popover a.copy-link').forEach((copybtn) => {
var tooltipInstance = new bootstrap.Tooltip(copybtn, {'title': copytext});
copybtn.addEventListener("click", function(e) { clipboard(this, tooltipInstance, e); }, false);
});
});
});

if (document.querySelectorAll('a.copy-popover-link').length > 0) {
document.addEventListener("click", function(e) {
document.querySelectorAll('a.copy-popover-link').forEach((sumbtn) => {
var popover = bootstrap.Popover.getInstance(sumbtn);
if (popover.tip && event.target.closest('.popover.show') == null && popover.tip.classList.contains('show')) {
popover.hide();
}
});
}, false);
}

document.querySelectorAll('a.copy-link').forEach((copybtn) => {
var tooltipInstance = new bootstrap.Tooltip(copybtn, {'title': copytext});
copybtn.addEventListener("click", function(e) { clipboard(this, tooltipInstance, e); }, false);
});

async function clipboard(copybtn, tooltipInstance, e) {
e.preventDefault();
var payload = copybtn.closest('p,td,div').querySelector('.copy-target').textContent.trimEnd();

try {
await navigator.clipboard.writeText(payload);
copybuttonupdate(copybtn, tooltipInstance, 'complete', copycomplete);
}
catch (err) {
copybuttonupdate(copybtn, tooltipInstance, 'error', copyerror);
}
}

function copybuttonupdate(copybtn, tooltipInstance, css, text) {
tooltipInstance.setContent({'.tooltip-inner': text});
copybtn.classList.add(css);
window.setTimeout(function() {
tooltipInstance.setContent({'.tooltip-inner': copytext});
copybtn.classList.remove(css);
}, 2500);
}
</script>
{% assign scripts = "theme" | split: "," %}
{% if page.scripts %}{% assign scripts = scripts | concat: page.scripts %}{% endif %}
{% for script in scripts %}
{% assign js = site.libs.js[script] %}

{% if site.environment == "dev" %}
<script type="module" src="{{ js.url | replace: '.min.', '.' }}" crossorigin="anonymous"></script>
{% else %}
<script type="module" src="{{ js.url }}" integrity="{{ js.sri }}" crossorigin="anonymous"></script>
{% endif %}
{% endfor %}
Loading
Loading