diff --git a/index.html b/index.html
index cda6dc8..df6d3f8 100644
--- a/index.html
+++ b/index.html
@@ -1,394 +1,396 @@
CodeMeta generator
Most fields are optional. Mandatory fields will be highlighted when generating Codemeta.
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 = `
`;
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');
}