diff --git a/_data/meta.yml b/_data/meta.yml index 61d7160ee..9957474b9 100644 --- a/_data/meta.yml +++ b/_data/meta.yml @@ -54,3 +54,7 @@ - name: redirect_from description: Relative path(s) to redirect to the license from, to prevent breaking old URLs required: false + +- name: annotations + description: Map of rule tags to excerpt strings that appear in the license body (enables in-text rule highlighting) + required: false diff --git a/_includes/footer.html b/_includes/footer.html index a4a549107..b22668ef3 100644 --- a/_includes/footer.html +++ b/_includes/footer.html @@ -27,6 +27,9 @@ }{% unless forloop.last %},{% endunless %} {% endfor %} ]; + {% if page.annotations %} + window.ruleAnnotations = {{ page.annotations | jsonify }}; + {% endif %} {% endif %} diff --git a/_layouts/license.html b/_layouts/license.html index 602089dd3..69f3cf8f1 100644 --- a/_layouts/license.html +++ b/_layouts/license.html @@ -45,6 +45,14 @@ + {% if page.annotations %} +
+ +
+ {% endif %} +{{ content | replace:"<", "<" | replace:">", ">" }}
diff --git a/_licenses/mit.txt b/_licenses/mit.txt
index 1ad0488f9..5eb19bd98 100644
--- a/_licenses/mit.txt
+++ b/_licenses/mit.txt
@@ -26,6 +26,14 @@ limitations:
- liability
- warranty
+annotations:
+ include-copyright:
+ - "The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software."
+ liability:
+ - "IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE"
+ warranty:
+ - "THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND"
+
---
MIT License
diff --git a/assets/css/application.scss b/assets/css/application.scss
index 11df9809e..5b48d2c22 100644
--- a/assets/css/application.scss
+++ b/assets/css/application.scss
@@ -568,6 +568,21 @@ strong {
.with-love { float: right; clear: right; }
+.rule-annotation {
+ background: rgba(20, 154, 212, 0.2);
+ border-radius: 2px;
+}
+
+.rule-annotation.is-focused {
+ background: rgba(61, 198, 55, 0.35);
+ outline: 1px solid #3dc637;
+}
+
+.license-annotations-toggle .button {
+ font-size: 0.85rem;
+ margin-bottom: 0.75rem;
+}
+
@media only screen and (-webkit-min-device-pixel-ratio: 2.0),
only screen and (min--moz-device-pixel-ratio: 2.0),
only screen and (-o-min-device-pixel-ratio: 200/100),
diff --git a/assets/js/app.js b/assets/js/app.js
index e8706dde1..36a587528 100644
--- a/assets/js/app.js
+++ b/assets/js/app.js
@@ -19,6 +19,7 @@ class Choosealicense {
this.initTooltips();
this.initClipboard();
this.initLicenseSuggestion();
+ this.initRuleAnnotations();
}
// Selects the content of a given element
@@ -138,6 +139,50 @@ class Choosealicense {
const licenseId = inputEl.getAttribute('data-license-id');
new LicenseSuggestion(inputEl, licenseId, statusIndicator);
}
+
+ initRuleAnnotations() {
+ const mapping = window.ruleAnnotations;
+ const pre = document.querySelector('#license-text');
+ const toggle = document.querySelector('#toggle-rule-annotations');
+ if (!mapping || !pre || !toggle) return;
+
+ const original = pre.textContent;
+ let active = false;
+
+ const applyHighlights = () => {
+ let html = original.replace(/&/g, '&').replace(//g, '>');
+ Object.entries(mapping).forEach(([tag, excerpts]) => {
+ excerpts.forEach((excerpt) => {
+ const escaped = excerpt.replace(/&/g, '&').replace(//g, '>');
+ const mark = `${escaped}`;
+ html = html.replace(escaped, mark);
+ });
+ });
+ pre.innerHTML = html;
+ };
+
+ const clearHighlights = () => {
+ pre.textContent = original;
+ };
+
+ toggle.addEventListener('click', () => {
+ active = !active;
+ toggle.setAttribute('aria-pressed', String(active));
+ toggle.textContent = active ? 'Hide rule highlights' : 'Highlight license rules in text';
+ if (active) applyHighlights();
+ else clearHighlights();
+ });
+
+ document.querySelectorAll('.license-rules li').forEach((item) => {
+ item.addEventListener('mouseenter', () => {
+ if (!active) return;
+ const tag = Array.from(item.classList).find((className) => mapping[className]);
+ if (!tag) return;
+ pre.querySelectorAll('.rule-annotation').forEach((el) => el.classList.remove('is-focused'));
+ pre.querySelectorAll(`.rule-annotation-${tag}`).forEach((el) => el.classList.add('is-focused'));
+ });
+ });
+ }
}
class LicenseSuggestion {
diff --git a/spec/license_annotations_spec.rb b/spec/license_annotations_spec.rb
new file mode 100644
index 000000000..417267054
--- /dev/null
+++ b/spec/license_annotations_spec.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe 'license rule annotations' do
+ licenses.each do |license|
+ next unless license['annotations']
+
+ context license['spdx-id'] do
+ it 'maps annotation keys to tags declared on the license' do
+ declared = (license['permissions'] + license['conditions'] + license['limitations']).uniq
+ extra = license['annotations'].keys - declared
+ expect(extra).to be_empty, "unknown annotation tags: #{extra.join(', ')}"
+ end
+
+ it 'references text that appears in the license body' do
+ body = license['content']
+ license['annotations'].each_value do |excerpts|
+ excerpts.each do |excerpt|
+ expect(body).to include(excerpt), "excerpt not found: #{excerpt[0, 60]}..."
+ end
+ end
+ end
+ end
+ end
+end