diff --git a/docs/2026-05-30/index.html b/docs/2026-05-30/index.html new file mode 100644 index 0000000..d4dbb44 --- /dev/null +++ b/docs/2026-05-30/index.html @@ -0,0 +1,733 @@ + + + + + + + + +RML Implementation Report + + + + + + + + + + + + + +
+ +

RML Implementation Report

+

Unofficial Draft

+
+ More details about this document +
+ +
Latest published version:
+ none +
+
Latest editor's draft:
https://kg-construct.github.io/implementation-report/
+
History:
+ Commit history +
+ + + + + +
+ Editor: +
+ David Chaves-Fraga + + + + (CiTIUS – University of Santiago de Compostela) +
+ + +
Feedback:
+ GitHub kg-construct/implementation-report + (pull requests, + new issue, + open issues) +
+ + +
+
+ + + +
+
+

Abstract

+

+ This document reports implementation results for the RML specification family across six modules: + RML-Core, RML-IO, RML-FNML, RML-CC, RML-LV, and RML-star. Engine metadata is loaded from this repository, while test-case metadata is pulled from the module repositories and engine results are loaded from one CSV file per engine. +

+
+ +

Status of This Document

+ This document is a draft of a potential specification. It has no official + standing of any kind and does not represent the support or consensus of + any standards organization. +

+

+ This is a living implementation report intended to be regenerated automatically from module test-case inventories and per-engine result CSV files, without requiring any server-side component. +

+
+ +

1. Introduction

+ +

+ The goal of this report is to provide a lightweight, static, GitHub-friendly implementation report for the RML test suite. The report groups test cases by module and computes implementation coverage automatically in the browser. +

+
+ +

2. Modules covered

+ + +
+ +

3. Processors

+ +
+
+

CSV data could not be loaded automatically.

+

Failed to fetch

+

If you open dev.html directly from your disk, the browser may block access to local or remote files.

+

For local testing, run a static server in this folder, for example:

+
python3 -m http.server 8000
+

and then open http://localhost:8000/dev.html.

+
+
+ +

4. Coverage summary by engine and module

+ +
+
+

CSV data could not be loaded automatically.

+

Failed to fetch

+

If you open dev.html directly from your disk, the browser may block access to local or remote files.

+

For local testing, run a static server in this folder, for example:

+
python3 -m http.server 8000
+

and then open http://localhost:8000/dev.html.

+
+
+ +

5. Implementation test results

+ +

5.1 RML-Core

+ +
+
+

CSV data could not be loaded automatically.

+

Failed to fetch

+

If you open dev.html directly from your disk, the browser may block access to local or remote files.

+

For local testing, run a static server in this folder, for example:

+
python3 -m http.server 8000
+

and then open http://localhost:8000/dev.html.

+
+
+

5.2 RML-IO

+ +
+
+

CSV data could not be loaded automatically.

+

Failed to fetch

+

If you open dev.html directly from your disk, the browser may block access to local or remote files.

+

For local testing, run a static server in this folder, for example:

+
python3 -m http.server 8000
+

and then open http://localhost:8000/dev.html.

+
+
+

5.3 RML-FNML

+ +
+
+

CSV data could not be loaded automatically.

+

Failed to fetch

+

If you open dev.html directly from your disk, the browser may block access to local or remote files.

+

For local testing, run a static server in this folder, for example:

+
python3 -m http.server 8000
+

and then open http://localhost:8000/dev.html.

+
+
+

5.4 RML-CC

+ +
+
+

CSV data could not be loaded automatically.

+

Failed to fetch

+

If you open dev.html directly from your disk, the browser may block access to local or remote files.

+

For local testing, run a static server in this folder, for example:

+
python3 -m http.server 8000
+

and then open http://localhost:8000/dev.html.

+
+
+

5.5 RML-LV

+ +
+
+

CSV data could not be loaded automatically.

+

Failed to fetch

+

If you open dev.html directly from your disk, the browser may block access to local or remote files.

+

For local testing, run a static server in this folder, for example:

+
python3 -m http.server 8000
+

and then open http://localhost:8000/dev.html.

+
+
+

5.6 RML-star

+ +
+
+

CSV data could not be loaded automatically.

+

Failed to fetch

+

If you open dev.html directly from your disk, the browser may block access to local or remote files.

+

For local testing, run a static server in this folder, for example:

+
python3 -m http.server 8000
+

and then open http://localhost:8000/dev.html.

+
+
+
+ + + + + + \ No newline at end of file diff --git a/docs/2026-05-30/resources/css/style.css b/docs/2026-05-30/resources/css/style.css new file mode 100644 index 0000000..f3cc129 --- /dev/null +++ b/docs/2026-05-30/resources/css/style.css @@ -0,0 +1,63 @@ +body { font-family: system-ui, sans-serif; } +table { width:100%; border-collapse:separate; border-spacing:0; margin:1rem 0 2rem; font-size:.95rem; table-layout:fixed; box-shadow:0 8px 22px rgba(15, 23, 42, 0.06); border-radius:10px; overflow:hidden; } +th, td { border-right:1px solid #d9d9d9; border-bottom:1px solid #d9d9d9; padding:.65rem .7rem; vertical-align:middle; overflow-wrap:anywhere; word-break:break-word; } +th:first-child, td:first-child { border-left:1px solid #d9d9d9; } +thead th { background:#eef3f8; color:#243447; font-weight:700; } +thead tr:first-child th { border-top:1px solid #d9d9d9; } +tbody tr:nth-child(even) td:first-child { background:#fafbfd; } +tbody tr:hover td:first-child { background:#eef6ff; } +section[id^="results-rml-"] { margin-top:1.5rem; } +section[id^="results-rml-"] > h3 { margin-bottom:.35rem; } +th:first-child, td:first-child { text-align:left; background:#fff; } +th:not(:first-child), td:not(:first-child) { text-align:center; } +th code, td code { white-space:normal; overflow-wrap:anywhere; word-break:break-word; } +.summary-table { table-layout:auto; } +.summary-table th:first-child, .summary-table td:first-child { min-width:12rem; } +.summary-table .summary-metric { + white-space: nowrap; + min-width: 0; + width: 1%; + padding-left: .45rem; + padding-right: .45rem; + font-size: .82rem; + letter-spacing: -.01em; +} +section[id^="results-rml-"] table th:first-child, +section[id^="results-rml-"] table td:first-child { width:15rem; min-width:15rem; } +.wrap-header { white-space:normal; line-height:1.15; } +.result-td { padding:0; text-align:center; } +.result-td-empty { padding:.65rem .7rem; } +.status { display:flex; align-items:center; justify-content:center; width:100%; min-height:3rem; padding:.7rem .35rem; font-weight:700; text-transform:lowercase; box-sizing:border-box; } +.status-passed { background:#1f7a1f; color:white; } +.status-failed { background:#b42318; color:white; } +.status-inapplicable { background:#6b7280; color:white; } +.result-cell { display:block; position:relative; cursor:default; } +.result-cell.is-empty { color:#7a7a7a; cursor:default; } +.result-note { + position: absolute; + top: .35rem; + right: .35rem; + display: inline-flex; + align-items: center; + justify-content: center; + width: 1.1rem; + min-width: 1.1rem; + height: 1.1rem; + padding: 0; + border: 0; + border-radius: 50%; + background: rgba(255,255,255,.24); + color: #fff; + font-size: .72rem; + line-height: 1; + font-weight: 800; + cursor: pointer; + appearance: none; + -webkit-appearance: none; +} +.result-popover { position:absolute; top:calc(100% + .35rem); right:.35rem; z-index:3; width:min(20rem, calc(100vw - 3rem)); padding:.65rem .75rem; border-radius:.6rem; background:#18212f; color:#fff; text-align:left; font-size:.85rem; line-height:1.35; box-shadow:0 10px 28px rgba(15, 23, 42, .28); } +.result-popover[hidden] { display:none; } +.result-td.note-open { overflow:visible; position:relative; z-index:2; } +.small { color:#555; font-size:.9rem; } +.summary-tail { white-space: nowrap; } +.error-box { border:1px solid #ef4444; background:#fef2f2; padding:1rem; border-radius:8px; } diff --git a/docs/2026-05-30/resources/js/app.js b/docs/2026-05-30/resources/js/app.js new file mode 100644 index 0000000..a0f57e8 --- /dev/null +++ b/docs/2026-05-30/resources/js/app.js @@ -0,0 +1,518 @@ +const VALID_STATUSES = ['passed', 'failed', 'inapplicable']; + +function csvTextToObjects(text) { + const cleaned = text.replace(/^\uFEFF/, '').replace(/\r\n/g, '\n').replace(/\r/g, '\n'); + if (!cleaned.trim()) return []; + + const rows = []; + let row = []; + let value = ''; + let inQuotes = false; + + for (let i = 0; i < cleaned.length; i++) { + const ch = cleaned[i]; + + if (ch === '"') { + if (inQuotes && cleaned[i + 1] === '"') { + value += '"'; + i++; + } else { + inQuotes = !inQuotes; + } + continue; + } + + if (ch === ',' && !inQuotes) { + row.push(value); + value = ''; + continue; + } + + if (ch === '\n' && !inQuotes) { + row.push(value); + if (row.some(cell => cell !== '')) rows.push(row); + row = []; + value = ''; + continue; + } + + value += ch; + } + + row.push(value); + if (row.some(cell => cell !== '')) rows.push(row); + if (!rows.length) return []; + + const headers = rows[0]; + return rows.slice(1).map(values => { + const obj = {}; + headers.forEach((header, index) => { + obj[header] = values[index] ?? ''; + }); + return obj; + }); +} + +async function fetchCsv(path) { + const response = await fetch(path, { cache: 'no-store' }); + if (!response.ok) { + const error = new Error(`Could not load ${path} (${response.status})`); + error.status = response.status; + error.path = path; + throw error; + } + return csvTextToObjects(await response.text()); +} + +function readEmbeddedData() { + const element = document.getElementById('report-data'); + if (!element) return null; + + try { + return JSON.parse(element.textContent); + } catch (error) { + console.error('Could not parse embedded report data', error); + return null; + } +} + +function escapeHtml(str) { + return String(str) + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); +} + +function plainCell(value, fallback = '-') { + return value ? escapeHtml(value) : fallback; +} + +function linkCell(url) { + if (!url) return '-'; + return `${escapeHtml(url)}`; +} + +function statusCell(status) { + return `${escapeHtml(status)}`; +} + +function formatModuleHeader(moduleName) { + if (moduleName.startsWith('RML-')) { + return `RML-
${escapeHtml(moduleName.slice(4))}`; + } + return escapeHtml(moduleName); +} + +function formatEngineHeader(name) { + const camelSplit = name.match(/^([A-Z]?[a-z0-9]+)([A-Z].*)$/); + if (camelSplit) { + return `${escapeHtml(camelSplit[1])}
${escapeHtml(camelSplit[2])}`; + } + + const hyphenIndex = name.indexOf('-'); + if (hyphenIndex > 0) { + return `${escapeHtml(name.slice(0, hyphenIndex))}
${escapeHtml(name.slice(hyphenIndex + 1))}`; + } + + return escapeHtml(name); +} + +function resultCell(result) { + if (!result) return '-'; + + const note = (result.notes || '').trim(); + const noteButton = note + ? `` + : ''; + return `${statusCell(result.status)}${noteButton}`; +} + +function normalizeSpecificationSlug(value) { + const normalized = String(value || '').trim().toLowerCase().replace(/\/+$/, ''); + const known = { + core: 'core', + io: 'io', + fnml: 'fnml', + cc: 'cc', + lv: 'lv', + 'rml-core': 'core', + 'rml-io': 'io', + 'rml-fnml': 'fnml', + 'rml-cc': 'cc', + 'rml-lv': 'lv', + 'http://w3id.org/rml/core': 'core', + 'http://w3id.org/rml/io': 'io', + 'http://w3id.org/rml/fnml': 'fnml', + 'http://w3id.org/rml/cc': 'cc', + 'http://w3id.org/rml/lv': 'lv' + }; + return known[normalized] || normalized; +} + +function titleCaseSpecification(specification) { + const slug = normalizeSpecificationSlug(specification); + if (!slug) return 'Unknown'; + if (slug === 'star') return 'RML-star'; + return `RML-${slug.toUpperCase()}`; +} + +function testcaseLink(testcaseId, specificationSlug) { + if (!testcaseId || !specificationSlug) return ''; + return `https://kg-construct.github.io/rml-${specificationSlug}/test-cases/docs/#${encodeURIComponent(testcaseId)}`; +} + +function normalizeTestcase(row, moduleInfo = null) { + const specificationSlug = normalizeSpecificationSlug(moduleInfo?.specification_slug || row.specification); + return { + testcase_id: row.ID || row.testcase_id || '', + module: moduleInfo?.module_name || titleCaseSpecification(specificationSlug), + title: row.title || '', + description: row.description || '', + specification: specificationSlug, + link: testcaseLink(row.ID || row.testcase_id || '', specificationSlug), + source_csv: moduleInfo?.testcases_csv || '', + error: String(row.error || '').toLowerCase() === 'true' + }; +} + +async function fetchModules() { + return fetchCsv('data/modules.csv'); +} + +async function fetchAllTestcases(modules) { + const settled = await Promise.allSettled( + modules.map(async moduleInfo => { + const rows = await fetchCsv(moduleInfo.testcases_csv); + return rows.map(row => normalizeTestcase(row, moduleInfo)); + }) + ); + + const notices = []; + const testcases = []; + + settled.forEach((entry, index) => { + const moduleInfo = modules[index]; + if (entry.status === 'fulfilled') { + testcases.push(...entry.value); + return; + } + + const error = entry.reason; + notices.push(`Could not load test cases for ${moduleInfo.module_name}: ${error.message}`); + }); + + return { testcases, notices }; +} + +function resolveResultsPath(processor) { + const configured = (processor.results_file || '').trim(); + const fallback = `results/${processor.processor_id}.csv`; + const path = configured || fallback; + + if ( + path.startsWith('data/') || + path.startsWith('./') || + path.startsWith('../') || + /^https?:\/\//.test(path) + ) { + return path; + } + + return `data/${path}`; +} + +async function fetchResultsForProcessor(processor) { + const path = resolveResultsPath(processor); + const rows = await fetchCsv(path); + return rows.map(row => ({ + ...row, + processor_id: row.processor_id || processor.processor_id + })); +} + +async function fetchAllResults(processors) { + const settled = await Promise.allSettled(processors.map(fetchResultsForProcessor)); + const notices = []; + const results = []; + + settled.forEach((entry, index) => { + const processor = processors[index]; + if (entry.status === 'fulfilled') { + results.push(...entry.value); + return; + } + + const error = entry.reason; + if (error && error.status === 404) { + notices.push(`No results file found for ${processor.name || processor.processor_id}: ${resolveResultsPath(processor)}`); + return; + } + + notices.push(`Could not load results for ${processor.name || processor.processor_id}: ${error.message}`); + }); + + return { results, notices }; +} + +async function loadLiveData() { + const [processors, modules] = await Promise.all([ + fetchCsv('data/processors.csv'), + fetchModules() + ]); + const { testcases, notices: testcaseNotices } = await fetchAllTestcases(modules); + const { results, notices: resultNotices } = await fetchAllResults(processors); + + return { + processors, + modules, + testcases, + results, + notices: [...testcaseNotices, ...resultNotices] + }; +} + +function buildProcessorTable(processors) { + const rows = processors.map(processor => ` + + ${escapeHtml(processor.name)} + ${plainCell(processor.version)} + ${plainCell(processor.release_date)} + ${plainCell(processor.contact)} + ${linkCell(processor.homepage)} + `).join(''); + + return `${rows}
NameVersionTest dateContactWeb page
`; +} + +function summarizeEngineByModule(processorId, processors, testcases, results) { + const moduleNames = [...new Set(testcases.map(testcase => testcase.module))]; + const summary = { + passed: 0, + failed: 0, + inapplicable: 0, + reported: 0, + modules: {} + }; + + moduleNames.forEach(moduleName => { + summary.modules[moduleName] = { covered: 0, total: 0, passed: 0, failed: 0, inapplicable: 0 }; + }); + + const testcaseMap = Object.fromEntries(testcases.map(testcase => [testcase.testcase_id, testcase])); + testcases.forEach(testcase => { + summary.modules[testcase.module].total++; + }); + + results + .filter(result => result.processor_id === processorId) + .forEach(result => { + const testcase = testcaseMap[result.testcase_id]; + if (!testcase) return; + + const moduleSummary = summary.modules[testcase.module]; + moduleSummary.covered++; + if (VALID_STATUSES.includes(result.status)) { + moduleSummary[result.status]++; + summary[result.status]++; + } + summary.reported++; + }); + + return summary; +} + +function buildSummaryTable(processors, testcases, results) { + const moduleNames = [...new Set(testcases.map(testcase => testcase.module))]; + const header = moduleNames.map(moduleName => `${formatModuleHeader(moduleName)}`).join(''); + + const rows = processors.map(processor => { + const summary = summarizeEngineByModule(processor.processor_id, processors, testcases, results); + const moduleCells = moduleNames.map(moduleName => { + const item = summary.modules[moduleName]; + return `${item.passed}/${item.total}
F ${item.failed} · I ${item.inapplicable}
`; + }).join(''); + + return ` + ${escapeHtml(processor.name)} + ${summary.reported} + ${summary.passed} + ${summary.failed} + ${summary.inapplicable} + ${moduleCells} + `; + }).join(''); + + if (!rows) { + return '

No engines found in data/processors.csv.

'; + } + + return `${header}${rows}
EngineReportedPassedFailedInappl.
`; +} + +function buildResultsTable(moduleName, testcases, processors, results) { + const moduleTestcases = testcases + .filter(testcase => testcase.module === moduleName) + .sort((a, b) => a.testcase_id.localeCompare(b.testcase_id)); + const resultMap = new Map( + results.map(result => [`${result.testcase_id}::${result.processor_id}`, result]) + ); + + const headerCells = processors + .map(processor => `${formatEngineHeader(processor.name)}`) + .join(''); + + const rows = moduleTestcases.map(testcase => { + const testcaseLabel = testcase.link + ? `${escapeHtml(testcase.testcase_id)}` + : `${escapeHtml(testcase.testcase_id)}`; + const processorCells = processors.map(processor => { + const result = resultMap.get(`${testcase.testcase_id}::${processor.processor_id}`); + const tdClass = result ? 'result-td' : 'result-td result-td-empty'; + return `${resultCell(result)}`; + }).join(''); + + return ` + ${testcaseLabel} + ${processorCells} + `; + }).join(''); + + if (!rows) { + return '

No matching results for this module.

'; + } + + return `${headerCells}${rows}
Test case
`; +} + +function renderResultsTables(testcases, processors, results) { + const moduleNames = [...new Set(testcases.map(testcase => testcase.module))]; + + document.querySelectorAll('[data-module-results]').forEach(container => { + const moduleName = container.getAttribute('data-module-results'); + const section = container.closest('section'); + + if (section) { + section.hidden = !moduleNames.includes(moduleName); + } + + container.innerHTML = moduleNames.includes(moduleName) + ? buildResultsTable(moduleName, testcases, processors, results) + : ''; + }); + + wireResultNotes(); +} + +function wireResultNotes() { + if (document.body.dataset.resultNotesBound === 'true') return; + document.body.dataset.resultNotesBound = 'true'; + + document.addEventListener('click', event => { + const trigger = event.target.closest('[data-note-trigger]'); + + document.querySelectorAll('.result-td.note-open').forEach(cell => { + if (trigger && cell.contains(trigger)) return; + cell.classList.remove('note-open'); + const popover = cell.querySelector('.result-popover'); + if (popover) popover.hidden = true; + }); + + if (!trigger) return; + + event.preventDefault(); + const cell = trigger.closest('.result-td'); + const popover = cell?.querySelector('.result-popover'); + if (!cell || !popover) return; + + const isOpen = cell.classList.contains('note-open'); + cell.classList.toggle('note-open', !isOpen); + popover.hidden = isOpen; + }); +} + +function setNotices() {} + +function collectDataNotices(testcases, processors, results) { + const testcaseIds = new Set(); + const processorIds = new Set(); + const notices = []; + + testcases.forEach(testcase => { + if (testcaseIds.has(testcase.testcase_id)) { + notices.push(`Duplicate test case id detected: ${testcase.testcase_id}`); + } + testcaseIds.add(testcase.testcase_id); + }); + + processors.forEach(processor => { + if (processorIds.has(processor.processor_id)) { + notices.push(`Duplicate engine id detected: ${processor.processor_id}`); + } + processorIds.add(processor.processor_id); + }); + + results.forEach(result => { + if (!processorIds.has(result.processor_id)) { + notices.push(`Result references an unknown engine id: ${result.processor_id}`); + } + if (!testcaseIds.has(result.testcase_id)) { + notices.push(`Result references an unknown test case id: ${result.testcase_id}`); + } + if (!VALID_STATUSES.includes(result.status)) { + notices.push(`Unexpected status "${result.status}" in result ${result.testcase_id}/${result.processor_id}`); + } + }); + + return [...new Set(notices)]; +} + +function setError(message) { + const html = ` +
+

CSV data could not be loaded automatically.

+

${escapeHtml(message)}

+

If you open dev.html directly from your disk, the browser may block access to local or remote files.

+

For local testing, run a static server in this folder, for example:

+
python3 -m http.server 8000
+

and then open http://localhost:8000/dev.html.

+
`; + + document.getElementById('processors-table').innerHTML = html; + document.getElementById('summary-table').innerHTML = html; + document.querySelectorAll('[data-module-results]').forEach(container => { + container.innerHTML = html; + const section = container.closest('section'); + if (section) { + section.hidden = false; + } + }); + setNotices(); +} + +async function init() { + try { + const embedded = readEmbeddedData(); + const liveData = embedded || await loadLiveData(); + const { processors, testcases, results } = liveData; + const notices = [ + ...(liveData.notices || []), + ...collectDataNotices(testcases, processors, results) + ]; + + document.getElementById('processors-table').innerHTML = buildProcessorTable(processors); + document.getElementById('summary-table').innerHTML = buildSummaryTable(processors, testcases, results); + setNotices(notices); + renderResultsTables(testcases, processors, results); + } catch (error) { + console.error(error); + setError(error.message || 'Unknown error'); + } +} + +if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', init, { once: true }); +} else { + init(); +} diff --git a/docs/index.html b/docs/index.html index 86f929f..d4dbb44 100644 --- a/docs/index.html +++ b/docs/index.html @@ -368,7 +368,7 @@

RML Implementation Report

- +