The JSON-LD Viewer is a browser-based application for viewing, editing, and validating any JSON-LD metadata with SHACL shapes. It works with any RDF vocabulary including DDI-CDI, schema.org, DCAT, BIBFRAME, and custom ontologies.
Critical: This document preserves architectural decisions to prevent AI context drift during development.
- Generic JSON-LD Support: Works with any vocabulary via standard jsonld.flatten()
- Dual Deployment: Standalone (GitHub Pages) + Dataverse integration
- SHACL with SPARQL Support: Core SHACL + SPARQL targets (vendored shacl-engine)
- Global State Pattern:
window.*properties for cross-file access - Client-Side Only: No backend required, all processing in browser
- Progressive Enhancement: Works without JavaScript for static display
- Configurable Defaults: Optional namespace and context mappings
User Action
↓
Load JSON-LD File
↓
Normalize to @graph Format (jsonld.js)
↓
Load SHACL Shapes (N3.js)
↓
Classify Properties (SHACL-defined vs EXTRA)
↓
Render UI with Color-Coded Badges
↓
[EDIT MODE ENABLED]
↓
User Modifications
↓
Validate Against SHACL (shacl-engine)
↓
Export or Save to Dataverse
Purpose: Enhanced search functionality with multiple modes and navigation
Global Variables (window.*):
- None (uses state.js getters/setters)
Key Functions:
performSearch()- Main search with scope filter integrationnavigateToMatch(direction)- Previous/next navigationmatchesSearch(text, term)- Case-sensitive and regex supportupdateSearchCounter()- Display match count ("X of Y")clearSearch()- Clear search with animationstoggleCaseSensitive()- Toggle case-sensitive modetoggleRegex()- Toggle regex modesetupAdvancedSearchHandlers()- Wire up all search controls
Integration:
- Called from event-handlers.js
- Uses filter state from advanced-filter.js (getFilterState())
- Applies CSS class
.current-search-matchwith pulse animation - Keyboard shortcuts: F3, Shift+F3, Enter
Purpose: Comprehensive filtering system with multiple filter types
Global Variables (window.*):
- None (uses state.js getters/setters)
Key Functions:
performSearch()- Execute search with current settingsnavigateToMatch(direction)- Move to next/previous matchclearSearch()- Clear search and highlightstoggleCaseSensitive()- Toggle case-sensitive modetoggleRegex()- Toggle regex modesetupAdvancedSearchHandlers()- Wire up all search controls
Search Features:
- Case-sensitive search toggle
- Regex pattern matching
- Previous/Next navigation
- Match counter (X of Y)
- Keyboard shortcuts (F3, Shift+F3, Enter)
Integration:
- Called from event-handlers.js
- Uses CSS classes:
.search-highlight,.current-search-match - Search state maintained in module scope
Purpose: Consistent UI for adding properties and root nodes
Key Functions:
renderUnifiedAddComponent(context)- Render unified add UIhandleUnifiedAdd()- Process add action- Context: "property" or "root-node"
Features:
- SHACL-defined items dropdown with descriptions
- Custom input with namespace selector
- "Add new namespace" option (opens modal)
- Enter key support
Purpose: Namespace management functionality
Key Functions:
renderNamespaceManager()- Display current namespacesaddNamespace(prefix, uri)- Add custom namespaceremoveNamespace(prefix)- Remove custom namespaceisBuiltInNamespace(prefix)- Check if prefix is built-in (protected)
Integration:
- Modal-based UI (no scroll)
- Integrates with unified-add-component
Purpose: Global configuration, initialization, Dataverse integration
Global Variables (window.*):
window.jsonData; // Current JSON-LD data (@graph format)
window.shaclShapes; // Raw SHACL shapes (Turtle string)
window.shaclShapesStore; // N3.Store with parsed SHACL triples
window.isEditMode; // Boolean: edit mode enabled?
window.originalData; // Original JSON-LD (for reset)
window.validationReport; // Latest SHACL validation report
window.fileId; // Dataverse file ID
window.siteUrl; // Dataverse instance URL
window.originalFileName; // File name for export
window.expandedJsonLd; // Fully expanded JSON-LD (for URI lookup)
window.currentShapeSource; // Currently loaded shape source
window.hadOriginalGraph; // Track if original data had @graph
window.SHAPE_URLS; // Map of shape source names to URLs
window.currentLogLevel; // Logging level (ERROR/WARN/INFO/DEBUG)
window.defaultTypeNamespace; // Default namespace for unprefixed types (configurable)State Properties (state.js):
state.isEmbeddedMode; // Boolean: true when callback parameter present (embedded in Dataverse iframe)
// Set in core.js when callback parameter detected:
// if (callbackParam) {
// setIsEmbeddedMode(true);
// }Configuration Variables:
// Default namespace for types without prefixes
// Automatically configured via detectAndConfigureDDICDIMode() when SHACL shapes load
// Can be manually overridden:
window.defaultTypeNamespace =
"http://ddialliance.org/Specification/DDI-CDI/1.0/RDF/"; // DDI-CDI
window.defaultTypeNamespace = "http://schema.org/"; // Schema.org
window.defaultTypeNamespace = "http://www.w3.org/ns/dcat#"; // DCAT
window.defaultTypeNamespace = null; // Generic mode (default before shapes load)Auto-Detection (js/cdi-shacl-loader.js):
// Automatically enable DDI-CDI mode when loading DDI-CDI shapes
// Detection is version-agnostic (1.0, 2.0, etc.) and protocol-agnostic (http/https)
function detectAndConfigureDDICDIMode(shapesText) {
const isDDICDI = /ddialliance\.org\/Specification\/DDI-CDI/i.test(shapesText);
if (isDDICDI) {
window.defaultTypeNamespace =
"http://ddialliance.org/Specification/DDI-CDI/1.0/RDF/";
// Enables: legacy context handling, DDICDIModels normalization
}
}Legacy Context Mappings (js/cdi-json-ld-helpers.js):
// Map legacy context URLs to local copies
const LEGACY_CONTEXT_URLS = {
"https://old-url.org/context.jsonld": "shapes/local-context.jsonld",
// Add more mappings as needed
};Why window.*?
- All JS files loaded via
<script>tags (no ES6 modules yet) - Variables must be truly global for cross-file access
- Alternative patterns (IIFE, namespaces) didn't work in iframe context
- Future: Will be refactored to ES6 modules with imports
Initialization:
- Parse URL parameters (fileId, siteUrl, testfile, debug)
- Set logging level based on ?debug=true
- Load JSON-LD file (from Dataverse API or local file)
- Load default SHACL shapes
- Pre-load local context for fallback
- Attach event handlers
- Render initial UI
Purpose: JSON-LD processing and context resolution
Key Functions:
normalizeToGraph(jsonLd)- Convert any JSON-LD to @graph formatresolvePrefix(context, prefix)- Resolve prefix to namespace URIexpandCompactIri(context, compactIri)- Expand "schema:Dataset" to full URIloadLocalContext()- Load and cache local DDI-CDI context
Critical Pattern:
// ✅ CORRECT: Handle array contexts
function resolvePrefix(context, prefix) {
if (Array.isArray(context)) {
for (const ctx of context) {
const ns = resolvePrefix(ctx, prefix);
if (ns) return ns;
}
return null;
}
// Handle string/object contexts...
}Common Pitfall:
- JSON-LD
@contextcan be string, object, or array - Must check all contexts in array before returning null
- External ontologies (prov, dcterms) may not be in main context
Purpose: Load and parse SHACL shapes from various sources
Shape Sources:
- DDI-CDI Official - ddi-cdi.github.io (300+ types, Core SHACL)
- CDIF Discovery Core - Local shapes/cdif-core.ttl (20 properties)
- Local Fallback - Built-in shapes/ddi-cdi-official.ttl
- Custom URL - User-provided Turtle file
Key Functions:
loadShaclShapes(source)- Load shapes from sourcecustomDocumentLoader(url)- Handle context loading with fallback
Document Loader Pattern:
// Handle external contexts gracefully
async function customDocumentLoader(url) {
try {
// Try working URL first
const response = await fetch(workingUrl, { timeout: 10000 });
if (response.ok) {
return await response.json();
}
} catch (error) {
// Fall back to local context
return await loadLocalContext();
}
}Critical: Only Core SHACL supported (no sh:SPARQLTarget, sh:SPARQLConstraint)
Purpose: Property classification and SHACL constraint extraction
Key Functions:
classifyProperty(nodeId, propertyName, shaclShapesStore, expandedJsonLd)- Returns "SHACL-defined" or "EXTRA"findNodeShape(nodeId, expandedJsonLd, shaclShapesStore)- Find matching sh:NodeShapefindPropertyShape(nodeShapeId, propertyUri, shaclShapesStore)- Get property constraintsgetEnumValues(propertyShapeId, shaclShapesStore)- Extract sh:in list values
Classification Logic:
- Get node type(s) from expandedJsonLd (full URIs)
- Find NodeShape(s) with matching
sh:targetClass - For each property, check if
sh:pathmatches property URI - Return "SHACL-defined" if found, "EXTRA" otherwise
Critical Pattern - N3.js Term Objects:
// ❌ WRONG: Using string URIs
const pathQuads = store.getQuads(propertyShapeRef.value, ...)
// ✅ CORRECT: Using term objects
const pathQuads = store.getQuads(propertyShapeRef, ...)Why This Matters:
- N3.js requires term objects (NamedNode, Literal), not strings
- Passing
propertyShapeRef.value(string) causes lookups to fail silently - This caused the "all properties marked EXTRA" bug
Named Property Shapes:
# CDIF shapes use this pattern:
cdifd:DatasetShape
sh:property cdifd:nameProperty . # Reference to named shape
cdifd:nameProperty
sh:path schema:name ;
sh:minCount 1 .When resolving: pass term object, not URI string.
Purpose: UI rendering and tree visualization
Key Functions:
renderData()- Main render loop for all nodesrenderNode(node, nodeTypes, nodeId)- Render single node cardrenderProperty(nodeId, key, value, nodeTypes)- Render property row with badge
Rendering Pattern:
// 1. Clear content
$("#content").empty();
// 2. Render nodes
jsonData["@graph"].forEach((node) => {
const html = renderNode(node, nodeTypes, nodeId);
$("#content").append(html);
});
// 3. Attach event handlers (NOT inside render functions)
attachEventHandlers();Property Badges:
- 🔵 Blue "SHACL-defined" - Property in SHACL shapes
- 🟡 Yellow "EXTRA" - Property not in shapes
- 🔴 Red text - Required but missing
- 🔷 Teal border - Modified value
Add Root Node Button:
- Only shown in edit mode
- Appears at top of content area (not toolbar)
- Prominent styling with dashed border box
Purpose: SHACL validation execution and UI updates
Key Function:
validateData()- Run SHACL validation and show results
Validation Flow:
- Convert JSON-LD to RDF (jsonld.toRDF)
- Parse RDF with N3.js into dataset
- Run shacl-engine validator (with SPARQL support)
- Parse validation report
- Update UI with violations/warnings
- Highlight invalid properties in red
SHACL Severity Levels:
sh:Violation(ERROR) - Missing required properties, datatype mismatchessh:Warning(WARNING) - Missing recommended propertiessh:Info(INFO) - Suggestions
Critical: Only Core SHACL constraints validated (no SPARQL)
Purpose: Suggest missing SHACL-defined properties
Key Function:
suggestProperties(nodeId, nodeTypes)- Return array of missing properties
Logic:
- Find all property shapes for node's types
- Filter out properties already in data
- Return suggestions with descriptions
Used By:
- Property dropdown in edit mode
- "Add Custom Property" feature
Purpose: All UI event handlers
Attached Events:
- Toggle Edit Mode button
- Save to Dataverse button
- Validate button
- Export JSON-LD button
- Collapse/Expand All buttons
- Search input
- Shape selector dropdown
- Load Local File button
- Property add/delete buttons
- Value edit inputs
Save Button Visibility Management:
// Global function for reactive button visibility
window.updateSaveButtonVisibility = function updateSaveButtonVisibility() {
const hasChanges = getChangedElementsCount() > 0;
const isEmbedded = getIsEmbeddedMode();
// Logic:
// - Standalone (!isEmbedded): Always show (can save to Dataverse anytime)
// - Embedded (isEmbedded): Show only when changes exist
if (!isEmbedded || hasChanges) {
$("#save-btn").removeClass("hidden");
} else {
$("#save-btn").addClass("hidden");
}
};Called from:
addChangedElement()in state.js (after every change)clearChangedElements()in state.js (after save/export)- Initial load in core.js (lines 242-244)
- After loading local file (event-handlers.js lines 123-127)
- After loading from Dataverse (event-handlers.js lines 313-317)
Purpose: Reactive button visibility based on mode and change tracking.
Critical Pattern:
// ✅ CORRECT: Attach handlers AFTER rendering
function attachEventHandlers() {
$("#toggle-edit-btn")
.off("click")
.on("click", function () {
window.isEditMode = !window.isEditMode;
// ...
});
}
// ❌ WRONG: Handlers inside render functions
// (causes duplicate handlers on re-render)Purpose: Extract modified data from DOM and prepare for export
Key Function:
extractDataFromDom()- Read all input values from DOM back into JSON-LD
Flow:
- Clone originalData
- For each node in DOM
- Read all input values
- Update JSON-LD structure
- Preserve @context and other metadata
- Return modified JSON-LD
Preserves:
- Original @context
- @graph vs flat structure (via
hadOriginalGraphflag) - Blank nodes and references
Purpose: Graph manipulation and node management
Key Functions:
getAvailableNodeTypes(shaclShapesStore)- List all sh:targetClass typescreateNewNode(type)- Create blank node with @id and @typeaddNodeToGraph(node)- Add node to window.jsonData['@graph']deleteNode(nodeId)- Remove node and clean up references
Node ID Generation:
// Create unique blank node IDs
const nodeId = `_:node-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;Reference Handling:
// Properties with sh:node or sh:class create references
{
"@id": "node1",
"schema:about": { "@id": "node2" } // Reference, not embed
}Input (flexible):
{
"@context": {...},
"@id": "dataset1",
"schema:name": "Example"
}Normalized to @graph:
{
"@context": {...},
"@graph": [
{
"@id": "dataset1",
"@type": "schema:Dataset",
"schema:name": "Example"
}
]
}Why @graph format?
- Allows multiple disconnected nodes
- Preserves references between nodes
- Standard format for RDF datasets
Purpose: Full URI resolution for property matching
Example:
// Compact
{ "schema:name": "Example" }
// Expanded (window.expandedJsonLd)
{ "http://schema.org/name": [{ "@value": "Example" }] }Usage:
- Property classification (match full URIs)
- Context-independent comparison
- SHACL shape path matching
-
N3.js v1.16.4 (~150KB)
- RDF/Turtle parsing
- Triple store for SHACL shapes
- Critical: Must use term objects, not strings
-
jsonld.js v8.3.2 (~130KB)
- JSON-LD normalization
- Expansion/compaction
- RDF conversion
-
shacl-engine v1.0.2 (~1.1MB)
- Core SHACL validation with SPARQL constraint support
- 15-26x faster than rdf-validate-shacl
- Bundled with all dependencies (including @comunica/query-sparql)
- Core SHACL validation only
- No SPARQL support
- Browser-compatible bundle
Total: ~400KB minified
-
jQuery 3.6.0
- DOM manipulation
- AJAX calls
- Event handling
-
Bootstrap 3.3.7
- UI components
- Grid system
- Modals
Why external?
- Dataverse already loads these
- Avoids duplication in iframe
- Keeps bundle small
Problem: editMode is not defined
Root Cause: Variable not on window object
Solution:
// ❌ WRONG
let editMode = false;
// ✅ CORRECT
window.isEditMode = false;Problem: Properties marked EXTRA when they should be SHACL-defined
Root Cause: Context is array but code only checks object
Solution:
// ❌ WRONG
const ns = context[prefix];
// ✅ CORRECT
const ns = resolvePrefix(context, prefix); // Handles arraysProblem: SHACL lookups fail silently
Root Cause: Passing string URI instead of term object
Solution:
// ❌ WRONG
const quads = store.getQuads(uri.value, ...);
// ✅ CORRECT
const quads = store.getQuads(uri, ...); // uri is NamedNodeProblem: Buttons trigger multiple times
Root Cause: Handlers attached inside render functions
Solution:
// ❌ WRONG
function renderNode() {
html += '<button onclick="..."></button>'; // Inline handlers
return html;
}
// ✅ CORRECT
function renderNode() {
html += '<button class="delete-btn" data-id="..."></button>';
return html;
}
function attachHandlers() {
$('.delete-btn').off('click').on('click', function() { ... });
}Problem: Properties don't match between data and shapes
Root Cause: Mixed http:// and https:// namespaces
Solution:
- Use
http://schema.org/consistently (not https) - schema.org canonical namespace is http://
- Check both data and shapes use same namespace
Input: js/core.js (entry point)
Output: dist/cdi-viewer.min.js (single bundle)
Bundled:
- All application JS files
- N3.js, jsonld.js, shacl-engine
External:
- jQuery (provided by Dataverse)
- Bootstrap (provided by Dataverse)
Minification:
- Terser plugin
- Keep console logs (drop_console: false)
- Mangle names except jQuery references
Standalone (GitHub Pages):
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js"></script>
<script src="dist/cdi-viewer.min.js"></script>Dataverse Integration:
<!-- jQuery/Bootstrap already loaded by Dataverse -->
<script src="lib/cdi-viewer.min.js"></script>Focus: Individual functions in isolation
Examples:
- Property classification logic
- Context resolution
- Node ID generation
- Data extraction from DOM
Focus: Module interactions
Examples:
- Load JSON-LD → normalize → render
- Edit property → extract → validate
- Load shapes → classify properties → show badges
Critical: Prevent bugs from recurring
Examples:
- window.isEditMode defined (not editMode)
- window.currentLogLevel accessible
- N3.js term objects used correctly
- Context arrays handled properly
Test Coverage Goal: 50% minimum (Jest configured)
Input Sanitization:
- All user input escaped before rendering
- jQuery
.text()instead of.html()for untrusted content - Property names validated against SHACL
Dataverse Save:
- API token in password input (not visible)
- Token not stored in localStorage
- HTTPS required for production
SHACL Shapes:
- Loaded from trusted sources only
- Validate Turtle syntax before parsing
- Timeout on external requests (10s)
Current: Script tags with window.* globals
Future: ES6 modules with imports
// core.js
export const config = { ... };
export let jsonData = null;
// render.js
import { config, jsonData } from './core.js';Benefits:
- Proper dependency management
- Tree shaking (smaller bundles)
- Better IDE support
- Type checking with TypeScript
Add type definitions:
- Function parameters
- Return types
- Global state interfaces
Benefits:
- Catch bugs at compile time
- Better IDE autocomplete
- Self-documenting code
sh:in constraints as dropdowns:
// Currently: text input
// Future: dropdown with sh:in values
<select>
<option>open</option>
<option>embargoed</option>
<option>restricted</option>
</select>State management:
- Track edit history
- Undo button restores previous state
- Redo button replays changes
Add ?debug=true to URL:
https://libis.github.io/cdi-viewer/?debug=true
Shows:
- Shape loading details
- Property classification decisions
- Validation execution logs
- Data structure information
1. Check global variables:
console.log("isEditMode:", window.isEditMode);
console.log("jsonData:", window.jsonData);
console.log("shaclShapesStore:", window.shaclShapesStore);2. Check shape loading:
console.log("Current shape source:", window.currentShapeSource);
console.log("Shape URLs:", window.SHAPE_URLS);
console.log("Shapes loaded:", window.shaclShapes ? "Yes" : "No");3. Check property classification:
const classification = classifyProperty(
nodeId,
propertyName,
window.shaclShapesStore,
window.expandedJsonLd
);
console.log(`Property ${propertyName}:`, classification);Useful commands:
// View all nodes
window.jsonData["@graph"];
// View expanded JSON-LD
window.expandedJsonLd;
// Re-render UI
renderData();
// Extract current data
const data = extractDataFromDom();
console.log(JSON.stringify(data, null, 2));Target: < 500KB minified
Current: ~1.14MB (App 38KB + Validation 1.1MB)
- Validation bundle includes shacl-engine with SPARQL support
- App bundle contains all application logic
Optimization:
- Core SHACL only (no SPARQL = saves 1.9MB)
- External jQuery/Bootstrap (saves ~100KB)
- Terser minification
- Gzip compression (server-side)
Current Limit: ~100 nodes perform well
For larger files:
- Consider pagination
- Virtual scrolling
- Lazy loading of nodes
- Search-based filtering
Strategy: Async + caching
// Load shapes once, reuse
if (!window.shaclShapesStore) {
await loadShaclShapes("ddi-cdi-official");
}- Write tests first (TDD)
- Update ARCHITECTURE.md
- Document in API.md
- Update README.md
- Add example to docs/
- Check existing tests
- Run
npm testbefore and after - Verify bundle size (
npm run build) - Test in both standalone and Dataverse modes
- Update documentation
- Tests added/updated?
- Documentation updated?
- Linting passes (
npm run lint)? - Bundle size acceptable?
- Works in standalone mode?
- Works in Dataverse iframe?
- No console errors?
- Accessible (ARIA labels)?
- DDI-CDI: https://ddi-cdi.github.io/
- SHACL: https://www.w3.org/TR/shacl/
- JSON-LD: https://json-ld.org/
- RDF: https://www.w3.org/RDF/
- N3.js: https://github.com/rdfjs/N3.js
- jsonld.js: https://github.com/digitalbazaar/jsonld.js
- shacl-engine: https://github.com/rdf-ext/shacl-engine (vendored with SPARQL target support)
shacl-engine v1.0.2 + SPARQL Targets
Located in vendor/shacl-engine/ - Modified version with SPARQL target support.
Why Vendored:
- Upstream doesn't yet support
sh:SPARQLTarget(SHACL Advanced Features) - Required for CDIF Discovery shapes that validate only root datasets
- Temporary until feature is merged upstream
Modifications (3 files, ~60 lines):
Validator.js- Addedsh:targetto shape detectionlib/Shape.js- MaderesolveTargets()asynclib/TargetResolver.js- Implemented SPARQL target execution
Integration:
package.json:"shacl-engine": "file:./vendor/shacl-engine"npm installcreates symlink:node_modules/shacl-engine -> vendor/shacl-engine- Rollup bundles from vendored source
- Works on GitHub Actions (vendor/ committed to git)
Migration Path:
- Submit PR to rdf-ext/shacl-engine
- Once merged and published, update to npm version
- Remove vendor/ directory
See VENDORED_DEPENDENCIES.md for details.
- API Guide: https://guides.dataverse.org/en/latest/api/
- External Tools: https://guides.dataverse.org/en/latest/api/external-tools.html
- GDCC Previewers: https://github.com/gdcc/dataverse-previewers
Last Updated: 2025-11-21
Maintained By: LIBIS @ KU Leuven