diff --git a/cypress/integration/persons.js b/cypress/integration/persons.js index f86426e..20f7799 100644 --- a/cypress/integration/persons.js +++ b/cypress/integration/persons.js @@ -1,5 +1,5 @@ /** - * Copyright (C) 2020 The Software Heritage developers + * Copyright (C) 2020-2025 The Software Heritage developers * See the AUTHORS file at the top-level directory of this distribution * License: GNU Affero General Public License version 3, or any later version * See top-level LICENSE file for more information @@ -395,31 +395,62 @@ describe('Author order change', function() { }); }); + it('moves roles with person', function() { + cy.get('#name').type('My Test Software'); + + cy.get('#author_add').click(); + cy.get('#author_1_givenName').type('Jane'); + + cy.get('#author_add').click(); + cy.get('#author_2_givenName').type('John'); + + cy.get('#author_1_role_add').click(); + cy.get('#author_1_roleName_0').type('Developer'); + cy.get('#author_1_roleName_0').should('have.value', 'Developer'); + + // Move author 1 to the right (swap with author 2) + cy.get('#author_1_moveToRight').click(); + + // After the swap, Jane (and her role) should be at author_2 + cy.get('#author_2_givenName').should('have.value', 'Jane'); + cy.get('#author_2_roleName_0').should('have.value', 'Developer'); + + // John should now be at author_1 and should not have the role + cy.get('#author_1_givenName').should('have.value', 'John'); + cy.get('#author_1_roleName_0').should('not.exist'); + }); + it('wraps around to the right', function() { cy.get('#name').type('My Test Software'); cy.get('#author_add').click(); cy.get('#author_add').click(); cy.get('#author_add').click(); - cy.get('#author_1_givenName').type('Jane'); + cy.get('#author_add').click(); + cy.get('#author_1_givenName').type('One'); cy.get('#author_1_affiliation').type('Example Org'); - cy.get('#author_2_givenName').type('John'); - cy.get('#author_2_familyName').type('Doe'); - cy.get('#author_3_givenName').type('Alex'); + cy.get('#author_2_givenName').type('Two'); + cy.get('#author_2_familyName').type('Too'); + cy.get('#author_3_givenName').type('Three'); + cy.get('#author_4_givenName').type('Four'); cy.get('#author_1_moveToLeft').click() - cy.get('#author_1_givenName').should('have.value', 'Alex'); - cy.get('#author_1_familyName').should('have.value', ''); + cy.get('#author_1_givenName').should('have.value', 'Two'); + cy.get('#author_1_familyName').should('have.value', 'Too'); cy.get('#author_1_affiliation').should('have.value', ''); - cy.get('#author_2_givenName').should('have.value', 'John'); - cy.get('#author_2_familyName').should('have.value', 'Doe'); + cy.get('#author_2_givenName').should('have.value', 'Three'); + cy.get('#author_2_familyName').should('have.value', ''); cy.get('#author_2_affiliation').should('have.value', ''); - cy.get('#author_3_givenName').should('have.value', 'Jane'); + cy.get('#author_3_givenName').should('have.value', 'Four'); cy.get('#author_3_familyName').should('have.value', ''); - cy.get('#author_3_affiliation').should('have.value', 'Example Org'); + cy.get('#author_3_affiliation').should('have.value', ''); + + cy.get('#author_4_givenName').should('have.value', 'One'); + cy.get('#author_4_familyName').should('have.value', ''); + cy.get('#author_4_affiliation').should('have.value', 'Example Org'); }); it('wraps around to the left', function() { @@ -428,25 +459,31 @@ describe('Author order change', function() { cy.get('#author_add').click(); cy.get('#author_add').click(); cy.get('#author_add').click(); - cy.get('#author_1_givenName').type('Jane'); + cy.get('#author_add').click(); + cy.get('#author_1_givenName').type('One'); cy.get('#author_1_affiliation').type('Example Org'); - cy.get('#author_2_givenName').type('John'); - cy.get('#author_2_familyName').type('Doe'); - cy.get('#author_3_givenName').type('Alex'); + cy.get('#author_2_givenName').type('Two'); + cy.get('#author_2_familyName').type('Too'); + cy.get('#author_3_givenName').type('Three'); + cy.get('#author_4_givenName').type('Four'); - cy.get('#author_3_moveToRight').click() + cy.get('#author_4_moveToRight').click() - cy.get('#author_1_givenName').should('have.value', 'Alex'); + cy.get('#author_1_givenName').should('have.value', 'Four'); cy.get('#author_1_familyName').should('have.value', ''); cy.get('#author_1_affiliation').should('have.value', ''); - cy.get('#author_2_givenName').should('have.value', 'John'); - cy.get('#author_2_familyName').should('have.value', 'Doe'); - cy.get('#author_2_affiliation').should('have.value', ''); + cy.get('#author_2_givenName').should('have.value', 'One'); + cy.get('#author_2_familyName').should('have.value', ''); + cy.get('#author_2_affiliation').should('have.value', 'Example Org'); - cy.get('#author_3_givenName').should('have.value', 'Jane'); - cy.get('#author_3_familyName').should('have.value', ''); - cy.get('#author_3_affiliation').should('have.value', 'Example Org'); + cy.get('#author_3_givenName').should('have.value', 'Two'); + cy.get('#author_3_familyName').should('have.value', 'Too'); + cy.get('#author_3_affiliation').should('have.value', ''); + + cy.get('#author_4_givenName').should('have.value', 'Three'); + cy.get('#author_4_familyName').should('have.value', ''); + cy.get('#author_4_affiliation').should('have.value', ''); }); }); @@ -889,6 +926,78 @@ describe('Multiple authors', function () { cy.get('#author_1_endDate_0').should('have.value', '2024-04-03'); cy.get('#author_2_givenName').should('have.value', 'Joe'); }); + + it('can remove the first one and reindexes remaining ones', function() { + cy.get('#name').type('My Test Software'); + + cy.get('#author_add').click(); + cy.get('#author_add').click(); + cy.get('#author_nb').should('have.value', '2'); + + cy.get('#author_1_givenName').type('Alice'); + cy.get('#author_2_givenName').type('Bob'); + + cy.get('#author_1_remove').click(); + + cy.get('#author_nb').should('have.value', '1'); + cy.get('#author_1_givenName').should('have.value', 'Bob'); + + cy.get('#generateCodemetaV2').click(); + cy.get('#codemetaText').then((elem) => JSON.parse(elem.text())) + .should('deep.equal', { + "@context": "https://doi.org/10.5063/schema/codemeta-2.0", + "type": "SoftwareSourceCode", + "name": "My Test Software", + "author": [ + { + "id": "_:author_1", + "type": "Person", + "givenName": "Bob" + } + ], + }); + }); + + it('can remove a middle one and reindexes remaining ones', function() { + cy.get('#name').type('My Test Software'); + + cy.get('#author_add').click(); + cy.get('#author_add').click(); + cy.get('#author_add').click(); + cy.get('#author_nb').should('have.value', '3'); + + cy.get('#author_1_givenName').type('Alice'); + cy.get('#author_2_givenName').type('Bob'); + cy.get('#author_3_givenName').type('Carol'); + + cy.get('#author_2_remove').click(); + + cy.get('#author_nb').should('have.value', '2'); + cy.get('#author_1_givenName').should('have.value', 'Alice'); + cy.get('#author_2_givenName').should('have.value', 'Carol'); + + cy.get('#generateCodemetaV2').click(); + cy.get('#codemetaText').then((elem) => JSON.parse(elem.text())) + .should('deep.equal', { + "@context": "https://doi.org/10.5063/schema/codemeta-2.0", + "type": "SoftwareSourceCode", + "name": "My Test Software", + "author": [ + { + "id": "_:author_1", + "type": "Person", + "givenName": "Alice" + }, + { + "id": "_:author_2", + "type": "Person", + "givenName": "Carol" + } + ], + }); + }); + + }); describe('Contributors', function () { diff --git a/index.html b/index.html index acaa027..eaf98fd 100644 --- a/index.html +++ b/index.html @@ -372,6 +372,8 @@

CodeMeta Generator v3.0

+ +
@@ -386,6 +388,8 @@

CodeMeta Generator v3.0

+ +
diff --git a/js/codemeta_generation.js b/js/codemeta_generation.js index b4e3c81..5f2e4b9 100644 --- a/js/codemeta_generation.js +++ b/js/codemeta_generation.js @@ -1,5 +1,5 @@ /** - * Copyright (C) 2019-2020 The Software Heritage developers + * Copyright (C) 2019-2025 The Software Heritage developers * See the AUTHORS file at the top-level directory of this distribution * License: GNU Affero General Public License version 3, or any later version * See top-level LICENSE file for more information @@ -7,6 +7,8 @@ "use strict"; +let codemetaGenerationEnabled = true; + const CODEMETA_CONTEXTS = { "2.0": { path: "./data/contexts/codemeta-2.0.jsonld", @@ -291,6 +293,12 @@ async function buildExpandedDocWithAllContexts() { // v2.0 is still default version for generation, for now async function generateCodemeta(codemetaVersion = "2.0") { + if (!codemetaGenerationEnabled) { + // Avoid regenerating a document while we are importing it. + // This avoid resetting the input in case there is an error in it. + return false; + } + var inputForm = document.querySelector('#inputForm'); var codemetaText, errorHTML; @@ -427,68 +435,73 @@ async function recompactDocWithAllContexts(doc) { } async function importCodemeta() { - var inputForm = document.querySelector('#inputForm'); - var doc = parseAndValidateCodemeta(false); + // Don't wipe the codemeta text (if any) in case of error + codemetaGenerationEnabled = false; - // Re-compact document with all contexts - // to allow importing property from any context - doc = await recompactDocWithAllContexts(doc); + try { + var doc = parseAndValidateCodemeta(false); + // Re-compact document with all contexts + // to allow importing property from any context + doc = await recompactDocWithAllContexts(doc); - resetForm(); + resetForm(); - if (doc['license'] !== undefined) { - if (typeof doc['license'] === 'string') { - doc['license'] = [doc['license']]; + if (doc['license'] !== undefined) { + if (typeof doc['license'] === 'string') { + doc['license'] = [doc['license']]; + } + + doc['license'].forEach(l => { + if (l.indexOf(SPDX_PREFIX) !== 0) { return; } + let licenseId = l.substring(SPDX_PREFIX.length); + insertLicenseElement(licenseId); + }); } - doc['license'].forEach(l => { - if (l.indexOf(SPDX_PREFIX) !== 0) { return; } - let licenseId = l.substring(SPDX_PREFIX.length); - insertLicenseElement(licenseId); + directCodemetaFields.forEach(function (item, index) { + setIfDefined('#' + item, doc[item]); }); - } - - directCodemetaFields.forEach(function (item, index) { - setIfDefined('#' + item, doc[item]); - }); - importShortOrg('#funder', doc["funder"]); - importReview(doc["review"]); - - // Import simple fields by joining on their separator - splittedCodemetaFields.forEach(function (item, index) { - const id = item[0]; - const separator = item[1]; - const serializer = item[2]; - const deserializer = item[3]; - let value = doc[id]; - if (value !== undefined) { - if (Array.isArray(value)) { - if (deserializer !== undefined) { - value = value.map((item) => deserializer(id, item)); - } - value = value.join(separator); - } else { - if (deserializer !== undefined) { - value = deserializer(id, value); + importShortOrg('#funder', doc["funder"]); + importReview(doc["review"]); + + // Import simple fields by joining on their separator + splittedCodemetaFields.forEach(function (item, index) { + const id = item[0]; + const separator = item[1]; + const deserializer = item[3]; + let value = doc[id]; + if (value !== undefined) { + if (Array.isArray(value)) { + if (deserializer !== undefined) { + value = value.map((item) => deserializer(id, item)); + } + value = value.join(separator); + } else { + if (deserializer !== undefined) { + value = deserializer(id, value); + } } + setIfDefined('#' + id, value); } - setIfDefined('#' + id, value); - } - }); - - for (const [key, items] of Object.entries(crossCodemetaFields)) { - let value = ""; - items.forEach(item => { - value = doc[item] || value; }); - setIfDefined(`#${key}`, value); - } - importPersons('author', 'Author', doc['author']); - if (doc['contributor']) { - // If only one contributor, it is compacted to an object - const contributors = Array.isArray(doc['contributor'])? doc['contributor'] : [doc['contributor']]; - importPersons('contributor', 'Contributor', contributors); + for (const [key, items] of Object.entries(crossCodemetaFields)) { + let value = ""; + items.forEach(item => { + value = doc[item] || value; + }); + setIfDefined(`#${key}`, value); + } + + importPersons('author', 'Author', doc['author']); + if (doc['contributor']) { + // If only one contributor, it is compacted to an object + const contributors = Array.isArray(doc['contributor']) ? doc['contributor'] : [doc['contributor']]; + importPersons('contributor', 'Contributor', contributors); + } + } finally { + // Re-enable codemeta generation + codemetaGenerationEnabled = true; } } diff --git a/js/dynamic_form.js b/js/dynamic_form.js index 8e79b4f..6d507ca 100644 --- a/js/dynamic_form.js +++ b/js/dynamic_form.js @@ -18,8 +18,7 @@ const personFields = [ function createPersonFieldset(personPrefix, legend) { // Creates a fieldset containing inputs for informations about a person - var fieldset = document.createElement("fieldset") - var moveButtons; + const fieldset = document.createElement("fieldset") fieldset.classList.add("person"); fieldset.classList.add("leafFieldset"); fieldset.id = personPrefix; @@ -29,6 +28,8 @@ function createPersonFieldset(personPrefix, legend) {
+
@@ -65,94 +66,183 @@ function createPersonFieldset(personPrefix, legend) { } function addPersonWithId(container, prefix, legend, id) { - var personPrefix = `${prefix}_${id}`; - var fieldset = createPersonFieldset(personPrefix, `${legend} #${id}`); + const personPrefix = `${prefix}_${id}`; + const fieldset = createPersonFieldset(personPrefix, legend); container.appendChild(fieldset); - document.querySelector(`#${personPrefix}_moveToLeft`) - .addEventListener('click', () => movePerson(prefix, id, "left")); - document.querySelector(`#${personPrefix}_moveToRight`) - .addEventListener('click', () => movePerson(prefix, id, "right")); - document.querySelector(`#${personPrefix}_role_add`) - .addEventListener('click', () => addRole(personPrefix)); + // Use ID selector to attach handlers that compute the current fieldset + // ID at click time, so renaming the IDs (when renumbering persons) + // won't break the handlers. + fieldset.querySelector('[id$="_moveToLeft"]') + .addEventListener('click', () => movePerson(prefix, fieldset.id, 'left')); + fieldset.querySelector('[id$="_moveToRight"]') + .addEventListener('click', () => movePerson(prefix, fieldset.id, 'right')); + fieldset.querySelector('[id$="_remove"]') + .addEventListener('click', () => removePerson(prefix, fieldset.id)); + fieldset.querySelector('[id$="_role_add"]') + .addEventListener('click', () => addRole(fieldset.id)); } -function movePerson(prefix, id1, direction) { - var nbPersons = getNbPersons(prefix); - var id2; +function movePerson(prefix, personPrefix, direction) { + const container = document.getElementById(`${prefix}_list`); + if (!container) return; - // Computer id2, the id of the person to flip id1 with (wraps around the - // end of the list of persons) - if (direction == "left") { - id2 = id1 - 1; - if (id2 <= 0) { - id2 = nbPersons; + const persons = Array.from(container.querySelectorAll('.person')); + const len = persons.length; + const currentIndex = persons.findIndex(function (p) { return p.id === personPrefix; }); + if (currentIndex === -1 || len <= 1) return; + + const currentElement = document.getElementById(personPrefix); + + function swapAdjacent(a, b) { + const parent = a && a.parentNode; + if (!parent) return; + if (a.nextElementSibling === b) { + parent.insertBefore(b, a); + } else if (b.nextElementSibling === a) { + parent.insertBefore(a, b); } } - else { - id2 = id1 + 1; - if (id2 > nbPersons) { - id2 = 1; + + const prev = currentElement.previousElementSibling; + const next = currentElement.nextElementSibling; + + if (direction === 'left') { + if (!prev) { + // Current is first element -> move to end (wrap-around) + container.appendChild(currentElement); + } else { + // Swap with previous element + swapAdjacent(prev, currentElement); + } + } else { + if (!next) { + // Current is last element -> move to beginning (wrap-around) + const firstEl = container.firstElementChild; + if (firstEl) container.insertBefore(currentElement, firstEl); + } else { + // Swap with next element + swapAdjacent(currentElement, next); } } - // Flip the field values, one by one - personFields.forEach((fieldName) => { - var field1 = document.querySelector(`#${prefix}_${id1}_${fieldName}`); - var field2 = document.querySelector(`#${prefix}_${id2}_${fieldName}`); - var value1 = field1.value; - var value2 = field2.value; - field2.value = value1; - field1.value = value2; - }); - - // Form was changed; regenerate + renumberPersons(prefix); generateCodemeta(); } function addPerson(prefix, legend) { - var container = document.querySelector(`#${prefix}_container`); - var personId = getNbPersons(prefix) + 1; + const container = document.getElementById(`${prefix}_list`); + const personId = getNbPersons(prefix) + 1; addPersonWithId(container, prefix, legend, personId); + renumberPersons(prefix); + return personId; +} - setNbPersons(prefix, personId); +function removePerson(prefix, personPrefix) { + const container = document.getElementById(`${prefix}_list`); + if (!container) return; + + if (personPrefix) { + const fs = document.getElementById(personPrefix); + if (!fs) return; + fs.remove(); + } else { + // If no personPrefix is provided, remove the last person + const last = container.lastElementChild; + if (!last) return; + last.remove(); + } - return personId; + renumberPersons(prefix); + generateCodemeta(); } -function removePerson(prefix) { - var personId = getNbPersons(prefix); +function renumberPersons(prefix) { + // Assume id pattern of "prefix_index_suffix" + function idSuffix(id) { + if (!id) return ''; + const parts = id.split('_'); + if (parts.length <= 2) return ''; + return parts.slice(2).join('_'); + } - document.querySelector(`#${prefix}_${personId}`).remove(); + const container = document.getElementById(`${prefix}_list`); + if (!container) return; - setNbPersons(prefix, personId - 1); + const persons = Array.from(container.querySelectorAll('.person')); + for (let i = 0; i < persons.length; i++) { + const fs = persons[i]; + const n = i + 1; + const newPersonPrefix = `${prefix}_${n}`; + + fs.id = newPersonPrefix; + + const legend = fs.querySelector('legend'); + if (legend) { + let base = legend.textContent.split('#')[0].trim(); + if (base === '') base = legend.textContent; + legend.textContent = base + ' #' + n; + } + + // Update descendant ids and names + Array.from(fs.querySelectorAll('[id]')).forEach(function (el) { + const oldId = el.id; + const suffix = idSuffix(oldId); + + el.id = `${newPersonPrefix}_${suffix}`; + + if (el.name) { + const nameSuffix = idSuffix(el.name); + el.name = `${newPersonPrefix}_${nameSuffix}`; + } + }); + + // Update label 'for' attributes + Array.from(fs.querySelectorAll('label[for]')).forEach(function (label) { + if (!label.htmlFor) return; + const forSuffix = idSuffix(label.htmlFor); + label.htmlFor = `${newPersonPrefix}_${forSuffix}`; + }); + } + + setNbPersons(prefix, persons.length); } // Initialize a group of persons (authors, contributors) on page load. // Useful if the page is reloaded. function initPersons(prefix, legend) { - var nbPersons = getNbPersons(prefix); - var personContainer = document.querySelector(`#${prefix}_container`) + const container = document.getElementById(`${prefix}_list`); + if (!container) return; + + // If there are already persons, do not add new ones, + // renumber them if needed. + const existing = Array.from(container.querySelectorAll('.person')); + if (existing.length > 0) { + renumberPersons(prefix); + return; + } - for (let personId = 1; personId <= nbPersons; personId++) { - addPersonWithId(personContainer, prefix, legend, personId); + // If no persons, add empty ones + const nbPersons = getNbPersons(prefix); + for (let i = 0; i < nbPersons; i++) { + addPerson(prefix, legend); } } function removePersons(prefix) { - var nbPersons = getNbPersons(prefix); - var personContainer = document.querySelector(`#${prefix}_container`) + const container = document.getElementById(`${prefix}_list`); + if (!container) return; - for (let personId = 1; personId <= nbPersons; personId++) { - removePerson(prefix) - } + container.innerHTML = ''; + setNbPersons(prefix, 0); + generateCodemeta(); } function addRole(personPrefix) { - const roleButtonGroup = document.querySelector(`#${personPrefix}_role_add`); - const roleIndexNode = document.querySelector(`#${personPrefix}_role_index`); + const roleButtonGroup = document.getElementById(`${personPrefix}_role_add`); + const roleIndexNode = document.getElementById(`${personPrefix}_role_index`); const roleIndex = parseInt(roleIndexNode.value, 10); const ul = document.createElement("ul") @@ -171,8 +261,11 @@ function addRole(personPrefix) { `; roleButtonGroup.after(ul); - document.querySelector(`#${personPrefix}_role_remove_${roleIndex}`) - .addEventListener('click', () => removeRole(personPrefix, roleIndex)); + document.getElementById(`${personPrefix}_role_remove_${roleIndex}`) + .addEventListener('click', (e) => { + const pid = e.currentTarget.closest?.('.person')?.id; + removeRole(pid, roleIndex); + }); roleIndexNode.value = roleIndex + 1; @@ -180,7 +273,8 @@ function addRole(personPrefix) { } function removeRole(personPrefix, roleIndex) { - document.querySelector(`#${personPrefix}_role_${roleIndex}`).remove(); + document.getElementById(`${personPrefix}_role_${roleIndex}`).remove(); + generateCodemeta(); } function resetForm() { @@ -191,7 +285,7 @@ function resetForm() { // Reset the form after deleting elements, so nbPersons doesn't get // reset before it's read. - document.querySelector('#inputForm').reset(); + document.getElementById('inputForm').reset(); } function fieldToLower(event) { @@ -207,41 +301,41 @@ function initCallbacks() { // In Firefox datalist selection without Enter press does not trigger // 'change' event, so we need to listen to 'input' event to catch // a selection with mouse click. - document.querySelector('#license') + document.getElementById('license') .addEventListener('input', validateLicense); - document.querySelector('#license') + document.getElementById('license') .addEventListener('change', validateLicense); // Safari needs 'keydown' to catch Enter press when datalist is shown - document.querySelector('#license') + document.getElementById('license') .addEventListener('keydown', validateLicense); - document.querySelector('#generateCodemetaV2').disabled = false; - document.querySelector('#generateCodemetaV2') + document.getElementById('generateCodemetaV2').disabled = false; + document.getElementById('generateCodemetaV2') .addEventListener('click', () => generateCodemeta("2.0")); - document.querySelector('#generateCodemetaV3').disabled = false; - document.querySelector('#generateCodemetaV3') + document.getElementById('generateCodemetaV3').disabled = false; + document.getElementById('generateCodemetaV3') .addEventListener('click', () => generateCodemeta("3.0")); - document.querySelector('#resetForm') + document.getElementById('resetForm') .addEventListener('click', resetForm); - document.querySelector('#validateCodemeta').disabled = false; - document.querySelector('#validateCodemeta') + document.getElementById('validateCodemeta').disabled = false; + document.getElementById('validateCodemeta') .addEventListener('click', () => parseAndValidateCodemeta(true)); - document.querySelector('#importCodemeta').disabled = false; - document.querySelector('#importCodemeta') + document.getElementById('importCodemeta').disabled = false; + document.getElementById('importCodemeta') .addEventListener('click', importCodemeta); document.querySelector('#downloadCodemeta input').disabled = false; document.querySelector('#downloadCodemeta input') .addEventListener('click', downloadCodemeta); - document.querySelector('#inputForm') + document.getElementById('inputForm') .addEventListener('change', () => generateCodemeta()); - document.querySelector('#developmentStatus') + document.getElementById('developmentStatus') .addEventListener('change', fieldToLower); initPersons('author', 'Author'); diff --git a/js/fields_data.js b/js/fields_data.js index 0d67894..8828fdb 100644 --- a/js/fields_data.js +++ b/js/fields_data.js @@ -65,7 +65,7 @@ function validateLicense(e) { 'insertReplacementText', // from datalist selection ]); if (!(e.inputType && CONFIRM_INPUT_TYPES.has(e.inputType))) { - // Typing characters, pasting, deletions - don't proceed + // Typing characters, pasting, deletions - don't proceed return; } } else { @@ -75,7 +75,7 @@ function validateLicense(e) { // Correct casing to the canonical SPDX license ID when possible. // This will allow user to type in any casing and hit Enter once - // to insert the license immediately. + // to insert the license immediately. const match = SPDX_LICENSE_IDS.find(id => id.toLowerCase() === license.toLowerCase()); if (match) { diff --git a/js/validation/index.js b/js/validation/index.js index 815a3b7..3519145 100644 --- a/js/validation/index.js +++ b/js/validation/index.js @@ -1,5 +1,5 @@ /** - * Copyright (C) 2020 The Software Heritage developers + * Copyright (C) 2020-2025 The Software Heritage developers * See the AUTHORS file at the top-level directory of this distribution * License: GNU Affero General Public License version 3, or any later version * See top-level LICENSE file for more information @@ -8,7 +8,7 @@ /* * Reads a CodeMeta file and shows human-friendly errors on it. * - * This validator intentionaly does not use a schema, in order to show errors + * This validator intentionally does not use a schema, in order to show errors * that are easy to understand for users with no understanding of JSON-LD. */ diff --git a/main.css b/main.css index e4cfaab..42abb3b 100644 --- a/main.css +++ b/main.css @@ -1,5 +1,5 @@ /** - * Copyright (C) 2019 The Software Heritage developers + * Copyright (C) 2019-2025 The Software Heritage developers * See the AUTHORS file at the top-level directory of this distribution * License: GNU Affero General Public License version 3, or any later version * See top-level LICENSE file for more information @@ -51,6 +51,7 @@ p input, p textarea { width: 100%; display: flex; justify-content: space-between; + align-items: center; } #license {