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
4 changes: 4 additions & 0 deletions _data/meta.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
3 changes: 3 additions & 0 deletions _includes/footer.html
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@
}{% unless forloop.last %},{% endunless %}
{% endfor %}
];
{% if page.annotations %}
window.ruleAnnotations = {{ page.annotations | jsonify }};
{% endif %}
{% endif %}
</script>
<script src="{{ 'assets/js/app.js' | relative_url }}"></script>
Expand Down
8 changes: 8 additions & 0 deletions _layouts/license.html
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,14 @@

</div>

{% if page.annotations %}
<p class="license-annotations-toggle">
<button type="button" id="toggle-rule-annotations" class="button" aria-pressed="false">
Highlight license rules in text
</button>
</p>
{% endif %}

<pre id="license-text">{{ content | replace:"<", "&lt;" | replace:">", "&gt;" }}</pre>

</div> <!-- /license-body -->
Expand Down
8 changes: 8 additions & 0 deletions _licenses/mit.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
15 changes: 15 additions & 0 deletions assets/css/application.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
45 changes: 45 additions & 0 deletions assets/js/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ class Choosealicense {
this.initTooltips();
this.initClipboard();
this.initLicenseSuggestion();
this.initRuleAnnotations();
}

// Selects the content of a given element
Expand Down Expand Up @@ -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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
Object.entries(mapping).forEach(([tag, excerpts]) => {
excerpts.forEach((excerpt) => {
const escaped = excerpt.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
const mark = `<mark class="rule-annotation rule-annotation-${tag}" data-rule="${tag}">${escaped}</mark>`;
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 {
Expand Down
26 changes: 26 additions & 0 deletions spec/license_annotations_spec.rb
Original file line number Diff line number Diff line change
@@ -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