From d5ce864206bf9f65576907ec79069c652f09c5b1 Mon Sep 17 00:00:00 2001 From: Tarek Lokal Date: Sat, 11 Jul 2026 12:41:59 +0200 Subject: [PATCH 1/2] Add OSI/FSF/OD approval badges (#827) Generate _data/approvals.yml and show badges on license pages. Co-authored-by: Cursor --- _data/approvals.yml | 235 ++++++++++++++++++++++++++++++++ _includes/approval-badges.html | 20 +++ _includes/license-overview.html | 9 ++ _includes/sidebar.html | 2 + script/generate_approvals.rb | 58 ++++++++ spec/approvals_spec.rb | 31 +++++ 6 files changed, 355 insertions(+) create mode 100644 _data/approvals.yml create mode 100644 _includes/approval-badges.html create mode 100755 script/generate_approvals.rb create mode 100644 spec/approvals_spec.rb diff --git a/_data/approvals.yml b/_data/approvals.yml new file mode 100644 index 000000000..085a602b0 --- /dev/null +++ b/_data/approvals.yml @@ -0,0 +1,235 @@ +0bsd: + spdx-id: 0BSD + osi: true + fsf: false + od: false +afl-3.0: + spdx-id: AFL-3.0 + osi: true + fsf: true + od: false +agpl-3.0: + spdx-id: AGPL-3.0 + osi: true + fsf: true + od: false +apache-2.0: + spdx-id: Apache-2.0 + osi: true + fsf: true + od: false +artistic-2.0: + spdx-id: Artistic-2.0 + osi: true + fsf: true + od: false +blueoak-1.0.0: + spdx-id: BlueOak-1.0.0 + osi: true + fsf: false + od: false +bsd-2-clause-patent: + spdx-id: BSD-2-Clause-Patent + osi: true + fsf: false + od: false +bsd-2-clause: + spdx-id: BSD-2-Clause + osi: true + fsf: false + od: false +bsd-3-clause-clear: + spdx-id: BSD-3-Clause-Clear + osi: false + fsf: true + od: false +bsd-3-clause: + spdx-id: BSD-3-Clause + osi: true + fsf: true + od: false +bsd-4-clause: + spdx-id: BSD-4-Clause + osi: false + fsf: true + od: false +bsl-1.0: + spdx-id: BSL-1.0 + osi: true + fsf: true + od: false +cc-by-4.0: + spdx-id: CC-BY-4.0 + osi: false + fsf: true + od: true +cc-by-sa-4.0: + spdx-id: CC-BY-SA-4.0 + osi: false + fsf: true + od: true +cc0-1.0: + spdx-id: CC0-1.0 + osi: false + fsf: true + od: true +cecill-2.1: + spdx-id: CECILL-2.1 + osi: true + fsf: false + od: false +cern-ohl-p-2.0: + spdx-id: CERN-OHL-P-2.0 + osi: true + fsf: false + od: false +cern-ohl-s-2.0: + spdx-id: CERN-OHL-S-2.0 + osi: true + fsf: false + od: false +cern-ohl-w-2.0: + spdx-id: CERN-OHL-W-2.0 + osi: true + fsf: false + od: false +ecl-2.0: + spdx-id: ECL-2.0 + osi: true + fsf: true + od: false +epl-1.0: + spdx-id: EPL-1.0 + osi: true + fsf: true + od: false +epl-2.0: + spdx-id: EPL-2.0 + osi: true + fsf: true + od: false +eupl-1.1: + spdx-id: EUPL-1.1 + osi: true + fsf: true + od: false +eupl-1.2: + spdx-id: EUPL-1.2 + osi: true + fsf: true + od: false +gfdl-1.3: + spdx-id: GFDL-1.3 + osi: false + fsf: true + od: false +gpl-2.0: + spdx-id: GPL-2.0 + osi: true + fsf: true + od: false +gpl-3.0: + spdx-id: GPL-3.0 + osi: true + fsf: true + od: false +isc: + spdx-id: ISC + osi: true + fsf: true + od: false +lgpl-2.1: + spdx-id: LGPL-2.1 + osi: true + fsf: true + od: false +lgpl-3.0: + spdx-id: LGPL-3.0 + osi: true + fsf: true + od: false +lppl-1.3c: + spdx-id: LPPL-1.3c + osi: true + fsf: false + od: false +mit-0: + spdx-id: MIT-0 + osi: true + fsf: false + od: false +mit: + spdx-id: MIT + osi: true + fsf: true + od: false +mpl-2.0: + spdx-id: MPL-2.0 + osi: true + fsf: true + od: false +ms-pl: + spdx-id: MS-PL + osi: true + fsf: true + od: false +ms-rl: + spdx-id: MS-RL + osi: true + fsf: true + od: false +mulanpsl-2.0: + spdx-id: MulanPSL-2.0 + osi: true + fsf: false + od: false +ncsa: + spdx-id: NCSA + osi: true + fsf: true + od: false +odbl-1.0: + spdx-id: ODbL-1.0 + osi: false + fsf: true + od: true +ofl-1.1: + spdx-id: OFL-1.1 + osi: true + fsf: true + od: false +osl-3.0: + spdx-id: OSL-3.0 + osi: true + fsf: true + od: false +postgresql: + spdx-id: PostgreSQL + osi: true + fsf: false + od: false +unlicense: + spdx-id: Unlicense + osi: true + fsf: true + od: false +upl-1.0: + spdx-id: UPL-1.0 + osi: true + fsf: true + od: false +vim: + spdx-id: Vim + osi: false + fsf: true + od: false +wtfpl: + spdx-id: WTFPL + osi: false + fsf: true + od: false +zlib: + spdx-id: Zlib + osi: true + fsf: true + od: false diff --git a/_includes/approval-badges.html b/_includes/approval-badges.html new file mode 100644 index 000000000..8cde58a86 --- /dev/null +++ b/_includes/approval-badges.html @@ -0,0 +1,20 @@ +{% assign approval = site.data.approvals[page.spdx-lcase] %} +{% if approval %} +
+

Approval

+
    + {% if approval.osi %} +
  • OSI approved
  • + {% endif %} + {% if approval.fsf %} +
  • FSF libre
  • + {% endif %} + {% if approval.od %} +
  • Open Definition conformant
  • + {% endif %} + {% unless approval.osi or approval.fsf or approval.od %} +
  • Not listed on OSI, FSF libre, or Open Definition catalogs used here.
  • + {% endunless %} +
+
+{% endif %} diff --git a/_includes/license-overview.html b/_includes/license-overview.html index c810fe2f0..412613d9c 100644 --- a/_includes/license-overview.html +++ b/_includes/license-overview.html @@ -13,6 +13,15 @@

+ {% assign overview_approval = site.data.approvals[include.license-id] %} + {% if overview_approval %} +

+ {% if overview_approval.osi %}OSI{% endif %} + {% if overview_approval.fsf %}FSF{% endif %} + {% if overview_approval.od %}OD{% endif %} +

+ {% endif %} +

{{ license.description }}

diff --git a/_includes/sidebar.html b/_includes/sidebar.html index 414c19106..289c4ad0e 100644 --- a/_includes/sidebar.html +++ b/_includes/sidebar.html @@ -12,6 +12,8 @@

Suggest this license

{% endunless %} + {% include approval-badges.html %} +

How to apply this license

diff --git a/script/generate_approvals.rb b/script/generate_approvals.rb new file mode 100755 index 000000000..b9a1da349 --- /dev/null +++ b/script/generate_approvals.rb @@ -0,0 +1,58 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +# Generates _data/approvals.yml from SPDX OSI flags and FSF/OD approval lists. +# Usage: script/generate_approvals.rb + +require 'yaml' +require 'json' +require 'open-uri' + +def spdx_list + url = 'https://spdx.org/licenses/licenses.json' + JSON.parse(OpenURI.open_uri(url).read)['licenses'].to_h { |l| [l['licenseId'], l] } +end + +def osi_ids(spdx) + spdx.select { |_id, meta| meta['isOsiApproved'] }.keys.map(&:downcase).to_h { |id| [id, true] } +end + +def fsf_ids + url = 'https://wking.github.io/fsf-api/licenses-full.json' + object = JSON.parse(OpenURI.open_uri(url).read) + ids = {} + object['licenses'].each_value do |meta| + next unless meta.dig('identifiers', 'spdx') && meta['tags']&.include?('libre') + + meta['identifiers']['spdx'].each { |identifier| ids[identifier.downcase] = true } + end + ids +end + +def od_ids + url = 'https://licenses.opendefinition.org/licenses/groups/od.json' + data = JSON.parse(OpenURI.open_uri(url).read) + data.keys.to_h { |id| [id.downcase, true] } +end + +spdx = spdx_list +osi = osi_ids(spdx) +fsf = fsf_ids +od = od_ids + +approvals = {} +Dir['_licenses/*.txt'].sort.each do |path| + slug = File.basename(path, '.txt') + front = File.read(path).split(/^---$/)[1] + meta = YAML.safe_load(front, permitted_classes: [Date, Time], aliases: true) + spdx_id = meta['spdx-id'] + key = spdx_id.downcase + approvals[slug] = { + 'spdx-id' => spdx_id, + 'osi' => !!osi[key], + 'fsf' => !!fsf[key], + 'od' => !!od[key] + } +end + +File.write('_data/approvals.yml', approvals.to_yaml) diff --git a/spec/approvals_spec.rb b/spec/approvals_spec.rb new file mode 100644 index 000000000..7f8ff0f4e --- /dev/null +++ b/spec/approvals_spec.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +require 'spec_helper' +require 'yaml' + +describe 'approvals data' do + let(:approvals_path) { File.expand_path '../_data/approvals.yml', __dir__ } + let(:approvals) { YAML.safe_load_file(approvals_path, permitted_classes: [Date, Time], aliases: true) } + + it 'has an entry for each catalog license' do + expect(approvals.keys.sort).to eq(licenses.map { |l| l['spdx-lcase'] }.sort) + end + + licenses.each do |license| + context license['spdx-id'] do + let(:entry) { approvals[license['spdx-lcase']] } + + it 'includes spdx-id and boolean flags' do + expect(entry['spdx-id']).to eq(license['spdx-id']) + expect(entry.keys).to contain_exactly('spdx-id', 'osi', 'fsf', 'od') + end + + it 'matches live OSI/FSF/OD data' do + slug = license['spdx-lcase'] + expect(entry['osi']).to eq(osi_approved_licenses.key?(slug)) + expect(entry['fsf']).to eq(fsf_approved_licenses.key?(slug)) + expect(entry['od']).to eq(od_approved_licenses.key?(slug)) + end + end + end +end From 39a048eaa441f3c06ab9cac517ffdeba2cd0e540 Mon Sep 17 00:00:00 2001 From: Tarek Lokal Date: Sat, 11 Jul 2026 12:42:06 +0200 Subject: [PATCH 2/2] Use page.slug for approval badge lookup Co-authored-by: Cursor --- _includes/approval-badges.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/_includes/approval-badges.html b/_includes/approval-badges.html index 8cde58a86..0cdcbb9f5 100644 --- a/_includes/approval-badges.html +++ b/_includes/approval-badges.html @@ -1,4 +1,4 @@ -{% assign approval = site.data.approvals[page.spdx-lcase] %} +{% assign approval = site.data.approvals[page.slug] %} {% if approval %}

Approval