diff --git a/index.html b/index.html index cda6dc8..df6d3f8 100644 --- a/index.html +++ b/index.html @@ -1,394 +1,396 @@ CodeMeta generator

CodeMeta generator v3.0

Most fields are optional. Mandatory fields will be highlighted when generating Codemeta.

The software itself

the software title


from SPDX licence list

Discoverability and citation


such as ISBNs, GTIN codes, UUIDs etc.. http://schema.org/identifier


grant funding software development


organization funding software development

Authors and contributors can be added below
Development community / tools


Run-time environment


Current version of the software


Editorial review

Additional Info


see www.repostatus.org for details

Authors
Contributors

Order of contributors does not matter.

+

codemeta.json:


   
diff --git a/js/codemeta_generation.js b/js/codemeta_generation.js index 6dd111d..6366558 100644 --- a/js/codemeta_generation.js +++ b/js/codemeta_generation.js @@ -1,437 +1,446 @@ /** * Copyright (C) 2019-2020 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 */ "use strict"; const LOCAL_CONTEXT_PATH = "./data/contexts/codemeta-local.jsonld"; const LOCAL_CONTEXT_URL = "local"; const CODEMETA_CONTEXTS = { "2.0": { path: "./data/contexts/codemeta-2.0.jsonld", url: "https://doi.org/10.5063/schema/codemeta-2.0" }, "3.0": { path: "./data/contexts/codemeta-3.0.jsonld", url: "https://w3id.org/codemeta/3.0" } } const SPDX_PREFIX = 'https://spdx.org/licenses/'; const loadContextData = async () => { const [contextLocal, contextV2, contextV3] = await Promise.all([ fetch(LOCAL_CONTEXT_PATH).then(response => response.json()), fetch(CODEMETA_CONTEXTS["2.0"].path).then(response => response.json()), fetch(CODEMETA_CONTEXTS["3.0"].path).then(response => response.json()) ]); return { [LOCAL_CONTEXT_URL]: contextLocal, [CODEMETA_CONTEXTS["2.0"].url]: contextV2, [CODEMETA_CONTEXTS["3.0"].url]: contextV3 } } const getJsonldCustomLoader = contexts => { return url => { const xhrDocumentLoader = jsonld.documentLoaders.xhr(); if (url in contexts) { return { contextUrl: null, document: contexts[url], documentUrl: url }; } return xhrDocumentLoader(url); } }; const initJsonldLoader = contexts => { jsonld.documentLoader = getJsonldCustomLoader(contexts); }; function emptyToUndefined(v) { if (v == null || v == "") return undefined; else return v; } function getIfSet(query) { return emptyToUndefined(document.querySelector(query).value); } function setIfDefined(query, value) { if (value !== undefined) { document.querySelector(query).value = value; } } function getLicenses() { let selectedLicenses = Array.from(document.getElementById("selected-licenses").children); return selectedLicenses.map(licenseDiv => SPDX_PREFIX + licenseDiv.children[0].innerText); } // Names of codemeta properties with a matching HTML field name const directCodemetaFields = [ 'codeRepository', 'contIntegration', 'dateCreated', 'datePublished', 'dateModified', 'downloadUrl', 'issueTracker', 'name', 'version', 'identifier', 'description', 'applicationCategory', 'releaseNotes', 'funding', 'developmentStatus', 'isSourceCodeOf', 'isPartOf', 'referencePublication' ]; const splittedCodemetaFields = [ ['keywords', ','], ['programmingLanguage', ','], ['runtimePlatform', ','], ['operatingSystem', ','], ['softwareRequirements', '\n'], ['relatedLink', '\n'], ] // Names of codemeta properties with a matching HTML field name, // in a Person object const directPersonCodemetaFields = [ 'givenName', 'familyName', 'email', 'affiliation', ]; const directRoleCodemetaFields = [ 'roleName', 'startDate', 'endDate', ]; const directReviewCodemetaFields = [ 'reviewAspect', 'reviewBody' ]; const crossCodemetaFields = { "contIntegration": ["contIntegration", "continuousIntegration"], // "embargoDate": ["embargoDate", "embargoEndDate"], Not present in the form yet TODO ? }; function generateShortOrg(fieldName) { var affiliation = getIfSet(fieldName); if (affiliation !== undefined) { if (isUrl(affiliation)) { return { "@type": "Organization", "@id": affiliation, }; } else { return { "@type": "Organization", "name": affiliation, }; } } else { return undefined; } } function generatePerson(idPrefix) { var doc = { "@type": "Person", } var id = getIfSet(`#${idPrefix}_id`); if (id !== undefined) { doc["@id"] = id; } directPersonCodemetaFields.forEach(function (item, index) { doc[item] = getIfSet(`#${idPrefix}_${item}`); }); doc["affiliation"] = generateShortOrg(`#${idPrefix}_affiliation`); return doc; } function generateRole(id) { const doc = { "@type": "Role" }; directRoleCodemetaFields.forEach(function (item, index) { doc[item] = getIfSet(`#${id} .${item}`); }); return doc; } function generateRoles(idPrefix, person) { const roles = []; const roleNodes = document.querySelectorAll(`ul[id^=${idPrefix}_role_`); roleNodes.forEach(roleNode => { const role = generateRole(roleNode.id); role["schema:author"] = person; // Prefix with "schema:" to prevent it from expanding into a list roles.push(role); }); return roles; } function generatePersons(prefix) { var persons = []; var nbPersons = getNbPersons(prefix); for (let personId = 1; personId <= nbPersons; personId++) { const idPrefix = `${prefix}_${personId}`; const person = generatePerson(idPrefix); persons.push(person); const roles = generateRoles(idPrefix, person); if (roles.length > 0) { persons = persons.concat(roles); } } return persons; } function generateReview() { const doc = { "@type": "Review" }; directReviewCodemetaFields.forEach(function (item, index) { doc[item] = getIfSet(`#${item}`); }); return doc; } async function buildExpandedJson() { var doc = { "@context": LOCAL_CONTEXT_URL, "@type": "SoftwareSourceCode", }; let licenses = getLicenses(); if (licenses.length > 0) { doc["license"] = licenses; } // Generate most fields directCodemetaFields.forEach(function (item, index) { doc[item] = getIfSet('#' + item) }); doc["funder"] = generateShortOrg('#funder', doc["affiliation"]); const review = generateReview(); if (review["reviewAspect"] || review["reviewBody"]) { doc["review"] = generateReview(); } // Generate simple fields parsed simply by splitting splittedCodemetaFields.forEach(function (item, index) { const id = item[0]; const separator = item[1]; const value = getIfSet('#' + id); if (value !== undefined) { doc[id] = value.split(separator).map(trimSpaces); } }); // Generate dynamic fields var authors = generatePersons('author'); if (authors.length > 0) { doc["author"] = authors; } var contributors = generatePersons('contributor'); if (contributors.length > 0) { doc["contributor"] = contributors; } for (const [key, items] of Object.entries(crossCodemetaFields)) { items.forEach(item => { doc[item] = doc[key]; }); } return await jsonld.expand(doc); } // v2.0 is still default version for generation, for now async function generateCodemeta(codemetaVersion = "2.0") { var inputForm = document.querySelector('#inputForm'); var codemetaText, errorHTML; if (inputForm.checkValidity()) { const expanded = await buildExpandedJson(); const compacted = await jsonld.compact(expanded, CODEMETA_CONTEXTS[codemetaVersion].url); codemetaText = JSON.stringify(compacted, null, 4); errorHTML = ""; } else { codemetaText = ""; errorHTML = "invalid input (see error above)"; inputForm.reportValidity(); } document.querySelector('#codemetaText').innerText = codemetaText; setError(errorHTML); // Run validator on the exported value, for extra validation. // If this finds a validation, it means there is a bug in our code (either // generation or validation), and the generation MUST NOT generate an // invalid codemeta file, regardless of user input. if (codemetaText && !validateDocument(JSON.parse(codemetaText))) { alert('Bug detected! The data you wrote is correct; but for some reason, it seems we generated an invalid codemeta.json. Please report this bug at https://github.com/codemeta/codemeta-generator/issues/new and copy-paste the generated codemeta.json file. Thanks!'); } if (codemetaText) { // For restoring the form state on page reload sessionStorage.setItem('codemetaText', codemetaText); } } // Imports a single field (name or @id) from an Organization. function importShortOrg(fieldName, doc) { if (doc !== undefined) { // Use @id if set, else use name setIfDefined(fieldName, doc["name"]); setIfDefined(fieldName, getDocumentId(doc)); } } function importReview(doc) { if (doc !== undefined) { directReviewCodemetaFields.forEach(item => { setIfDefined('#' + item, doc[item]); }); } } function authorsEqual(author1, author2) { // TODO should test more properties for equality? return author1.givenName === author2.givenName && author1.familyName === author2.familyName && author1.email === author2.email; } function getSingleAuthorsFromRoles(docs) { return docs.filter(doc => getDocumentType(doc) === "Role") .map(doc => doc["schema:author"]) .reduce((authorSet, currentAuthor) => { const foundAuthor = authorSet.find(author => authorsEqual(author, currentAuthor)); if (!foundAuthor) { return authorSet.concat([currentAuthor]); } else { return authorSet; } }, []); } function importRoles(personPrefix, roles) { roles.forEach(role => { const roleId = addRole(`${personPrefix}`); directRoleCodemetaFields.forEach(item => { setIfDefined(`#${personPrefix}_${item}_${roleId}`, role[item]); }); }); } function importPersons(prefix, legend, docs) { if (docs === undefined) { return; } const authors = docs.filter(doc => getDocumentType(doc) === "Person"); const authorsFromRoles = getSingleAuthorsFromRoles(docs); const allAuthorDocs = authors.concat(authorsFromRoles) .reduce((authors, currentAuthor) => { if (!authors.find(author => authorsEqual(author, currentAuthor))) { authors.push(currentAuthor); } return authors; }, []); allAuthorDocs.forEach(function (doc, index) { var personId = addPerson(prefix, legend); setIfDefined(`#${prefix}_${personId}_id`, getDocumentId(doc)); directPersonCodemetaFields.forEach(function (item, index) { setIfDefined(`#${prefix}_${personId}_${item}`, doc[item]); }); importShortOrg(`#${prefix}_${personId}_affiliation`, doc['affiliation']); const roles = docs.filter(currentDoc => getDocumentType(currentDoc) === "Role") .filter(currentDoc => authorsEqual(currentDoc["schema:author"], doc)); importRoles(`${prefix}_${personId}`, roles); }); } async function importCodemeta() { var inputForm = document.querySelector('#inputForm'); var doc = await parseAndValidateCodemeta(false); resetForm(); 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); }); } 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]; let value = doc[id]; if (value !== undefined) { if (Array.isArray(value)) { value = value.join(separator); } 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']) importPersons('contributor', 'Contributor', doc['contributor']) } function loadStateFromStorage() { var codemetaText = sessionStorage.getItem('codemetaText') if (codemetaText) { document.querySelector('#codemetaText').innerText = codemetaText; importCodemeta(); } } + +function downloadCodemeta() { + const codemetaText = document.querySelector('#codemetaText').innerText; + const blob = new Blob([codemetaText], {type: 'application/json'}); + const url = URL.createObjectURL(blob); + document.querySelector('#downloadCodemeta').href = url; + document.querySelector('#downloadCodemeta').download = "codemeta.json"; + URL.revokeObjectURL(url); +} diff --git a/js/dynamic_form.js b/js/dynamic_form.js index f15aba8..dc56d45 100644 --- a/js/dynamic_form.js +++ b/js/dynamic_form.js @@ -1,232 +1,236 @@ /** * Copyright (C) 2019 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 */ "use strict"; // List of all HTML fields in a Person fieldset. const personFields = [ 'givenName', 'familyName', 'email', 'id', 'affiliation', ]; function createPersonFieldset(personPrefix, legend) { // Creates a fieldset containing inputs for informations about a person var fieldset = document.createElement("fieldset") var moveButtons; fieldset.classList.add("person"); fieldset.classList.add("leafFieldset"); fieldset.id = personPrefix; fieldset.innerHTML = ` ${legend}

`; return fieldset; } function addPersonWithId(container, prefix, legend, id) { var personPrefix = `${prefix}_${id}`; var fieldset = createPersonFieldset(personPrefix, `${legend} #${id}`); 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)); } function movePerson(prefix, id1, direction) { var nbPersons = getNbPersons(prefix); var id2; // 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; } } else { id2 = id1 + 1; if (id2 > nbPersons) { id2 = 1; } } // 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 generateCodemeta(); } function addPerson(prefix, legend) { var container = document.querySelector(`#${prefix}_container`); var personId = getNbPersons(prefix) + 1; addPersonWithId(container, prefix, legend, personId); setNbPersons(prefix, personId); return personId; } function removePerson(prefix) { var personId = getNbPersons(prefix); document.querySelector(`#${prefix}_${personId}`).remove(); setNbPersons(prefix, personId - 1); } // 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`) for (let personId = 1; personId <= nbPersons; personId++) { addPersonWithId(personContainer, prefix, legend, personId); } } function removePersons(prefix) { var nbPersons = getNbPersons(prefix); var personContainer = document.querySelector(`#${prefix}_container`) for (let personId = 1; personId <= nbPersons; personId++) { removePerson(prefix) } } function addRole(personPrefix) { const roleButtonGroup = document.querySelector(`#${personPrefix}_role_add`); const roleIndexNode = document.querySelector(`#${personPrefix}_role_index`); const roleIndex = parseInt(roleIndexNode.value, 10); const ul = document.createElement("ul") ul.classList.add("role"); ul.id = `${personPrefix}_role_${roleIndex}`; ul.innerHTML = `
  • `; roleButtonGroup.after(ul); document.querySelector(`#${personPrefix}_role_remove_${roleIndex}`) .addEventListener('click', () => removeRole(personPrefix, roleIndex)); roleIndexNode.value = roleIndex + 1; return roleIndex; } function removeRole(personPrefix, roleIndex) { document.querySelector(`#${personPrefix}_role_${roleIndex}`).remove(); } function resetForm() { removePersons('author'); removePersons('contributor'); // Reset the list of selected licenses document.getElementById("selected-licenses").innerHTML = ''; // Reset the form after deleting elements, so nbPersons doesn't get // reset before it's read. document.querySelector('#inputForm').reset(); } function fieldToLower(event) { event.target.value = event.target.value.toLowerCase(); } function initCallbacks() { document.querySelector('#license') .addEventListener('change', validateLicense); document.querySelector('#generateCodemetaV2').disabled = false; document.querySelector('#generateCodemetaV2') .addEventListener('click', () => generateCodemeta("2.0")); document.querySelector('#generateCodemetaV3').disabled = false; document.querySelector('#generateCodemetaV3') .addEventListener('click', () => generateCodemeta("3.0")); document.querySelector('#resetForm') .addEventListener('click', resetForm); document.querySelector('#validateCodemeta').disabled = false; document.querySelector('#validateCodemeta') .addEventListener('click', () => parseAndValidateCodemeta(true)); document.querySelector('#importCodemeta').disabled = false; document.querySelector('#importCodemeta') .addEventListener('click', importCodemeta); + document.querySelector('#downloadCodemeta input').disabled = false; + document.querySelector('#downloadCodemeta input') + .addEventListener('click', downloadCodemeta); + document.querySelector('#inputForm') .addEventListener('change', () => generateCodemeta()); document.querySelector('#developmentStatus') .addEventListener('change', fieldToLower); initPersons('author', 'Author'); initPersons('contributor', 'Contributor'); }