Skip to content

Commit 715c1f0

Browse files
committed
Add public/docs/ospometa-yaml-generator.html
1 parent a89c5f6 commit 715c1f0

File tree

1 file changed

+380
-0
lines changed

1 file changed

+380
-0
lines changed
Lines changed: 380 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,380 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<title>OSPOmeta YAML Generator</title>
5+
<style>
6+
/* Base styles */
7+
body {
8+
font-family: monospace;
9+
max-width: 800px;
10+
margin: 20px auto;
11+
}
12+
13+
/* Form components */
14+
.form__group {
15+
margin-bottom: 10px;
16+
}
17+
.form__label {
18+
display: inline-block;
19+
width: 160px;
20+
font-weight: bold;
21+
}
22+
.form__input {
23+
width: 610px;
24+
padding: 5px;
25+
}
26+
.form__input--textarea {
27+
height: 100px;
28+
vertical-align: top;
29+
}
30+
.form__input--invalid {
31+
border: 1px solid red;
32+
background-color: #ffeeee;
33+
}
34+
.form__error {
35+
color: red;
36+
font-size: 0.8em;
37+
margin-left: 170px;
38+
display: none;
39+
}
40+
41+
/* Output area */
42+
.output {
43+
border: 1px solid #ddd;
44+
padding: 10px;
45+
margin-top: 20px;
46+
min-height: 200px;
47+
white-space: pre;
48+
font-family: monospace;
49+
background-color: #f5f5f5;
50+
}
51+
52+
/* Action buttons */
53+
.button-container {
54+
margin-top: 10px;
55+
text-align: right;
56+
}
57+
.button {
58+
padding: 8px 15px;
59+
background-color: #4CAF50;
60+
color: white;
61+
border: none;
62+
border-radius: 4px;
63+
cursor: pointer;
64+
font-size: 16px;
65+
}
66+
.button:hover {
67+
background-color: #45a049;
68+
}
69+
</style>
70+
</head>
71+
<body>
72+
<h1>OSPOmeta YAML Generator</h1>
73+
74+
<p>OSPOmeta aims at providing a minimal metadata schema for Open Source Programme Offices. See <a href="https://github.com/Bluehats/ospometa">this repo for more</a>.</p>
75+
76+
<br/>
77+
78+
<div id="form-container">
79+
<!-- Form fields will be dynamically generated here -->
80+
</div>
81+
82+
<h2>Generated YAML for this OSPO</h2>
83+
<div id="result-div" class="output">
84+
# YAML will be generated here as you type
85+
</div>
86+
<div class="button-container">
87+
<button id="email-button" class="button">Send YAML by Email</button>
88+
</div>
89+
90+
<script>
91+
// Constants for validation messages and other strings
92+
const MESSAGES = {
93+
URL_ERROR: 'Please enter a valid URL',
94+
EMAIL_ERROR: 'Please enter a valid email address',
95+
DATE_ERROR: 'Please enter a valid ISO 8601 date (YYYY-MM-DD)',
96+
CODE_ERROR: 'One or more URLs are invalid',
97+
EMPTY_URL: '# Enter a URL to generate YAML',
98+
EMAIL_SUBJECT: 'New yml entry for the list of public sector OSPOs',
99+
EMAIL_RECIPIENT: '[email protected]'
100+
};
101+
102+
// Form field configuration - single source of truth
103+
const formFields = [
104+
{
105+
id: 'url-input',
106+
type: 'text',
107+
label: 'URL',
108+
placeholder: 'The main URL of the OSPO',
109+
validation: 'url',
110+
errorMsg: MESSAGES.URL_ERROR,
111+
yamlKey: null // Special case - used as identifier
112+
},
113+
{
114+
id: 'name-input',
115+
type: 'text',
116+
label: 'Name',
117+
placeholder: 'The name of the OSPO',
118+
yamlKey: 'name'
119+
},
120+
{
121+
id: 'country-input',
122+
type: 'text',
123+
label: 'Country',
124+
placeholder: 'Country code (e.g., FR, US, DE)',
125+
validation: 'countryCode',
126+
errorMsg: 'Please enter a valid 2-letter country code',
127+
yamlKey: 'country'
128+
},
129+
{
130+
id: 'description-input',
131+
type: 'text',
132+
label: 'Description',
133+
placeholder: 'The description of the OSPO',
134+
yamlKey: 'description'
135+
},
136+
{
137+
id: 'email-input',
138+
type: 'email',
139+
label: 'Email',
140+
placeholder: 'The contact email',
141+
validation: 'email',
142+
errorMsg: MESSAGES.EMAIL_ERROR,
143+
yamlKey: 'email'
144+
},
145+
{
146+
id: 'created-input',
147+
type: 'text',
148+
label: 'Created',
149+
placeholder: 'The creation date of the OSPO (e.g., 2023-05-17)',
150+
validation: 'date',
151+
errorMsg: MESSAGES.DATE_ERROR,
152+
yamlKey: 'created'
153+
},
154+
{
155+
id: 'floss-policy-input',
156+
type: 'text',
157+
label: 'FLOSS policy',
158+
placeholder: 'The URL of the FLOSS policy published by the OSPO',
159+
validation: 'url',
160+
errorMsg: MESSAGES.URL_ERROR,
161+
yamlKey: 'floss_policy'
162+
},
163+
{
164+
id: 'code-input',
165+
type: 'textarea',
166+
label: 'Code',
167+
placeholder: 'A list of source code organizations/groups (one per line)',
168+
validation: 'codeUrls',
169+
errorMsg: MESSAGES.CODE_ERROR,
170+
yamlKey: 'code',
171+
list: true
172+
}
173+
];
174+
175+
// Cache DOM references
176+
const domElements = {
177+
form: document.getElementById('form-container'),
178+
result: document.getElementById('result-div'),
179+
emailButton: document.getElementById('email-button')
180+
};
181+
182+
// Validation functions
183+
const validators = {
184+
url: function(value) {
185+
if (!value) return true; // Empty is valid
186+
try {
187+
// First check with built-in URL parser
188+
const url = new URL(value);
189+
190+
// Additional validation checks
191+
// 1. Must be http or https protocol
192+
if (url.protocol !== 'http:' && url.protocol !== 'https:') {
193+
return false;
194+
}
195+
196+
// 2. Must have a valid hostname
197+
if (!url.hostname || url.hostname.indexOf('.') === -1) {
198+
return false;
199+
}
200+
201+
// 3. The URL should be properly formed
202+
// This regex checks for a basic URL pattern with protocol and domain
203+
const urlPattern = /^(https?:\/\/)(www\.)?[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)+(\/[a-zA-Z0-9-_.~%/]*)*$/;
204+
return urlPattern.test(value);
205+
206+
} catch (e) {
207+
return false;
208+
}
209+
},
210+
211+
email: function(value) {
212+
if (!value) return true; // Empty is valid
213+
const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
214+
return regex.test(value);
215+
},
216+
217+
date: function(value) {
218+
if (!value) return true; // Empty is valid
219+
const regex = /^\d{4}-\d{2}-\d{2}$/;
220+
if (!regex.test(value)) return false;
221+
222+
const date = new Date(value);
223+
return date instanceof Date && !isNaN(date) &&
224+
date.toISOString().slice(0, 10) === value;
225+
},
226+
227+
codeUrls: function(value) {
228+
if (!value) return true; // Empty is valid
229+
const urls = value.split('\n').filter(item => item.trim());
230+
return urls.every(url => validators.url(url.trim()));
231+
},
232+
233+
countryCode: function(value) {
234+
if (!value) return true; // Empty is valid
235+
const regex = /^[A-Z]{2}$/;
236+
return regex.test(value);
237+
}
238+
};
239+
240+
// Generate HTML for the form
241+
function generateFormHTML() {
242+
let html = '';
243+
244+
formFields.forEach(field => {
245+
html += `
246+
<div class="form__group">
247+
<label for="${field.id}" class="form__label">${field.label}:</label>
248+
${field.type === 'textarea'
249+
? `<textarea id="${field.id}" class="form__input form__input--textarea" placeholder="${field.placeholder}" aria-describedby="${field.id}-error"></textarea>`
250+
: `<input type="${field.type}" id="${field.id}" class="form__input" placeholder="${field.placeholder}" aria-describedby="${field.id}-error">`
251+
}
252+
${field.validation
253+
? `<div id="${field.id}-error" class="form__error">${field.errorMsg}</div>`
254+
: ''
255+
}
256+
</div>
257+
`;
258+
});
259+
260+
return html;
261+
}
262+
263+
// Validate a specific field
264+
function validateField(fieldId) {
265+
const field = formFields.find(f => f.id === fieldId);
266+
if (!field || !field.validation) return true;
267+
268+
const input = document.getElementById(fieldId);
269+
const errorElement = document.getElementById(`${fieldId}-error`);
270+
const isValid = validators[field.validation](input.value.trim());
271+
272+
input.classList.toggle('form__input--invalid', !isValid);
273+
if (errorElement) {
274+
errorElement.style.display = isValid ? 'none' : 'block';
275+
}
276+
277+
return isValid;
278+
}
279+
280+
// Validate all fields
281+
function validateAllFields() {
282+
return formFields
283+
.filter(field => field.validation)
284+
.every(field => validateField(field.id));
285+
}
286+
287+
// Get all form values
288+
function getFormValues() {
289+
const values = {};
290+
291+
formFields.forEach(field => {
292+
const element = document.getElementById(field.id);
293+
if (element) {
294+
values[field.id] = element.value.trim();
295+
}
296+
});
297+
298+
return values;
299+
}
300+
301+
// Generate YAML from form values
302+
function generateYaml() {
303+
const values = getFormValues();
304+
const url = values['url-input'];
305+
306+
if (!url) {
307+
return MESSAGES.EMPTY_URL;
308+
}
309+
310+
let yaml = `${url}:\n`;
311+
312+
formFields.forEach(field => {
313+
if (field.yamlKey && values[field.id]) {
314+
if (field.list) {
315+
yaml += ` ${field.yamlKey}:\n`;
316+
const items = values[field.id].split('\n').filter(item => item.trim());
317+
items.forEach(item => {
318+
yaml += ` - ${item.trim()}\n`;
319+
});
320+
} else if (field.yamlKey === 'description') {
321+
yaml += ` ${field.yamlKey}:\n en: ${values[field.id]}\n`;
322+
} else {
323+
yaml += ` ${field.yamlKey}: ${values[field.id]}\n`;
324+
}
325+
}
326+
});
327+
328+
return yaml;
329+
}
330+
331+
// Update the YAML display
332+
function updateYaml() {
333+
domElements.result.textContent = generateYaml();
334+
}
335+
336+
// Initialize the form
337+
function initForm() {
338+
// Generate form HTML
339+
domElements.form.innerHTML = generateFormHTML();
340+
341+
// Set up event delegation for form inputs
342+
domElements.form.addEventListener('input', function(e) {
343+
if (e.target.matches('input, textarea')) {
344+
updateYaml();
345+
}
346+
});
347+
348+
domElements.form.addEventListener('blur', function(e) {
349+
if (e.target.matches('input, textarea')) {
350+
validateField(e.target.id);
351+
}
352+
}, true);
353+
354+
// Set up email button
355+
domElements.emailButton.addEventListener('click', function() {
356+
if (!validateAllFields()) {
357+
alert('Please correct the validation errors before sending the email.');
358+
return;
359+
}
360+
361+
const yaml = generateYaml();
362+
if (yaml === MESSAGES.EMPTY_URL) {
363+
alert('Please enter at least a URL before sending the email.');
364+
return;
365+
}
366+
367+
const subject = encodeURIComponent(MESSAGES.EMAIL_SUBJECT);
368+
const body = encodeURIComponent(yaml);
369+
window.location.href = `mailto:${MESSAGES.EMAIL_RECIPIENT}?subject=${subject}&body=${body}`;
370+
});
371+
372+
// Initial YAML generation
373+
updateYaml();
374+
}
375+
376+
// Initialize the application
377+
initForm();
378+
</script>
379+
</body>
380+
</html>

0 commit comments

Comments
 (0)