1346 lines
46 KiB
HTML
1346 lines
46 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>XSD-Based XML Viewer</title>
|
|
<style>
|
|
body {
|
|
font-family: Arial, sans-serif;
|
|
max-width: 1400px;
|
|
margin: 0 auto;
|
|
padding: 80px 20px;
|
|
line-height: 1.6;
|
|
}
|
|
|
|
/* Fixed top banner */
|
|
.top-banner {
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
height: 60px;
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
color: white;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
|
z-index: 1000;
|
|
font-size: 18px;
|
|
font-weight: bold;
|
|
}
|
|
|
|
/* Fixed bottom banner */
|
|
.bottom-banner {
|
|
position: fixed;
|
|
bottom: 0;
|
|
left: 0;
|
|
right: 0;
|
|
height: 50px;
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
color: white;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
box-shadow: 0 -2px 10px rgba(0,0,0,0.1);
|
|
z-index: 1000;
|
|
font-size: 14px;
|
|
}
|
|
|
|
.upload-container {
|
|
display: flex;
|
|
gap: 20px;
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
.upload-area {
|
|
flex: 1;
|
|
border: 2px dashed #ccc;
|
|
border-radius: 8px;
|
|
padding: 40px;
|
|
text-align: center;
|
|
background-color: #f9f9f9;
|
|
}
|
|
|
|
.upload-area.dragover {
|
|
border-color: #007bff;
|
|
background-color: #e3f2fd;
|
|
}
|
|
|
|
.upload-area.has-file {
|
|
border-color: #28a745;
|
|
background-color: #e8f5e9;
|
|
}
|
|
|
|
.file-status {
|
|
margin-top: 10px;
|
|
font-weight: bold;
|
|
}
|
|
|
|
.file-status.loaded {
|
|
color: #28a745;
|
|
}
|
|
|
|
.file-status.error {
|
|
color: #dc3545;
|
|
}
|
|
|
|
.group-container {
|
|
margin: 20px 0;
|
|
border: 1px solid #ddd;
|
|
border-radius: 8px;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.group-header {
|
|
background: linear-gradient(135deg, #f5f5f5 0%, #e0e0e0 100%);
|
|
padding: 15px;
|
|
font-size: 18px;
|
|
font-weight: bold;
|
|
color: #333;
|
|
border-bottom: 2px solid #007bff;
|
|
}
|
|
|
|
.element-card {
|
|
background: #fff;
|
|
padding: 20px;
|
|
margin: 0;
|
|
border-bottom: 3px solid #ddd;
|
|
position: relative;
|
|
}
|
|
|
|
.element-card:last-child {
|
|
border-bottom: none;
|
|
}
|
|
|
|
.element-card:not(:last-child)::after {
|
|
content: '';
|
|
position: absolute;
|
|
bottom: -3px;
|
|
left: 50%;
|
|
transform: translateX(-50%);
|
|
width: 50px;
|
|
height: 3px;
|
|
background: linear-gradient(90deg, transparent, #007bff, transparent);
|
|
}
|
|
|
|
.field-row {
|
|
display: flex;
|
|
margin: 8px 0;
|
|
align-items: flex-start;
|
|
}
|
|
|
|
.field-label {
|
|
min-width: 180px;
|
|
font-weight: bold;
|
|
color: #5c6bc0;
|
|
padding-right: 15px;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.field-value {
|
|
flex: 1;
|
|
color: #333;
|
|
word-wrap: break-word;
|
|
}
|
|
|
|
.element-pair {
|
|
position: relative;
|
|
}
|
|
|
|
.element-pair.show-xml .element-xml {
|
|
display: block;
|
|
}
|
|
|
|
.element-xml {
|
|
display: none;
|
|
background: #f8f9fa;
|
|
border: 1px solid #dee2e6;
|
|
border-radius: 4px;
|
|
padding: 15px;
|
|
margin-top: 15px;
|
|
font-family: 'Courier New', Consolas, monospace;
|
|
font-size: 12px;
|
|
white-space: pre;
|
|
overflow-x: auto;
|
|
max-height: 400px;
|
|
overflow-y: auto;
|
|
}
|
|
|
|
.element-index {
|
|
position: absolute;
|
|
top: 20px;
|
|
right: 20px;
|
|
background: #5c6bc0;
|
|
color: white;
|
|
padding: 4px 12px;
|
|
border-radius: 15px;
|
|
font-size: 14px;
|
|
font-weight: bold;
|
|
}
|
|
|
|
.attribute-list {
|
|
margin-top: 10px;
|
|
padding-top: 10px;
|
|
border-top: 1px solid #ddd;
|
|
}
|
|
|
|
.attribute-item {
|
|
display: flex;
|
|
gap: 10px;
|
|
margin: 5px 0;
|
|
font-size: 14px;
|
|
}
|
|
|
|
.attribute-name {
|
|
font-weight: bold;
|
|
color: #666;
|
|
min-width: 100px;
|
|
}
|
|
|
|
.attribute-value {
|
|
color: #333;
|
|
}
|
|
|
|
/* Custom formatting tags */
|
|
code, kbd, samp, var, .code {
|
|
font-family: 'Courier New', Consolas, monospace;
|
|
background-color: #f0f0f0;
|
|
padding: 2px 4px;
|
|
border-radius: 3px;
|
|
font-size: 0.9em;
|
|
}
|
|
|
|
kbd {
|
|
background-color: #333;
|
|
color: #fff;
|
|
border: 1px solid #666;
|
|
border-radius: 3px;
|
|
box-shadow: 0 1px 0 rgba(0,0,0,0.2);
|
|
}
|
|
|
|
samp {
|
|
background-color: #e8f5e9;
|
|
color: #2e7d32;
|
|
border-left: 3px solid #4caf50;
|
|
padding-left: 6px;
|
|
}
|
|
|
|
var {
|
|
background-color: #fff3e0;
|
|
color: #e65100;
|
|
font-style: italic;
|
|
}
|
|
|
|
mark, .highlight {
|
|
background-color: #ffeb3b;
|
|
padding: 2px 4px;
|
|
}
|
|
|
|
.critical {
|
|
color: #d32f2f;
|
|
font-weight: bold;
|
|
background-color: #ffebee;
|
|
padding: 2px 4px;
|
|
border-radius: 3px;
|
|
}
|
|
|
|
.optional {
|
|
color: #9e9e9e;
|
|
font-style: italic;
|
|
}
|
|
|
|
.deprecated {
|
|
text-decoration: line-through;
|
|
color: #757575;
|
|
}
|
|
|
|
.todo {
|
|
background-color: #fff9c4;
|
|
color: #f57f17;
|
|
padding: 2px 6px;
|
|
border-left: 3px solid #fbc02d;
|
|
font-weight: bold;
|
|
}
|
|
|
|
.value {
|
|
color: #1976d2;
|
|
font-weight: bold;
|
|
}
|
|
|
|
.unit {
|
|
color: #666;
|
|
font-size: 0.9em;
|
|
}
|
|
|
|
.term {
|
|
color: #7b1fa2;
|
|
font-weight: 600;
|
|
border-bottom: 1px dotted #7b1fa2;
|
|
}
|
|
|
|
.acronym {
|
|
text-transform: uppercase;
|
|
font-size: 0.9em;
|
|
letter-spacing: 0.5px;
|
|
}
|
|
|
|
.version {
|
|
background-color: #e3f2fd;
|
|
color: #1565c0;
|
|
padding: 2px 6px;
|
|
border-radius: 3px;
|
|
font-family: monospace;
|
|
}
|
|
|
|
blockquote {
|
|
margin: 10px 0;
|
|
padding-left: 15px;
|
|
border-left: 4px solid #ccc;
|
|
background-color: #f9f9f9;
|
|
font-style: italic;
|
|
color: #555;
|
|
}
|
|
|
|
pre {
|
|
background-color: #f5f5f5;
|
|
padding: 10px;
|
|
border-radius: 4px;
|
|
overflow-x: auto;
|
|
font-family: 'Courier New', Consolas, monospace;
|
|
}
|
|
|
|
.ref, .url {
|
|
color: #1976d2;
|
|
text-decoration: underline;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.controls {
|
|
text-align: center;
|
|
margin: 20px 0;
|
|
}
|
|
|
|
.controls button {
|
|
background-color: #007bff;
|
|
color: white;
|
|
border: none;
|
|
padding: 10px 20px;
|
|
border-radius: 4px;
|
|
cursor: pointer;
|
|
font-size: 14px;
|
|
margin: 0 5px;
|
|
}
|
|
|
|
.controls button:hover {
|
|
background-color: #0056b3;
|
|
}
|
|
|
|
.controls button:disabled {
|
|
background-color: #ccc;
|
|
cursor: not-allowed;
|
|
}
|
|
|
|
.error {
|
|
color: #d32f2f;
|
|
background-color: #ffebee;
|
|
padding: 10px;
|
|
border-radius: 4px;
|
|
margin: 10px 0;
|
|
}
|
|
|
|
.info-box {
|
|
background: #e3f2fd;
|
|
padding: 15px;
|
|
border-radius: 4px;
|
|
margin: 20px 0;
|
|
}
|
|
|
|
.schema-info {
|
|
background: #f0f8ff;
|
|
padding: 10px;
|
|
border-radius: 4px;
|
|
margin: 10px 0;
|
|
font-size: 14px;
|
|
}
|
|
|
|
.schema-info h3 {
|
|
margin-top: 0;
|
|
color: #007bff;
|
|
}
|
|
|
|
/* Dark mode styles */
|
|
@media (prefers-color-scheme: dark) {
|
|
body {
|
|
background-color: #1a1a1a;
|
|
color: #e0e0e0;
|
|
}
|
|
|
|
.upload-area {
|
|
background-color: #2d2d2d;
|
|
border-color: #555;
|
|
color: #e0e0e0;
|
|
}
|
|
|
|
.upload-area.dragover {
|
|
border-color: #4fc3f7;
|
|
background-color: #1e3a8a;
|
|
}
|
|
|
|
.upload-area.has-file {
|
|
border-color: #4caf50;
|
|
background-color: #1b5e20;
|
|
}
|
|
|
|
.group-container {
|
|
border-color: #555;
|
|
}
|
|
|
|
.group-header {
|
|
background: linear-gradient(135deg, #2d2d2d 0%, #1a1a1a 100%);
|
|
color: #e0e0e0;
|
|
border-bottom-color: #4fc3f7;
|
|
}
|
|
|
|
.element-card {
|
|
background: #2d2d2d;
|
|
border-bottom-color: #555;
|
|
}
|
|
|
|
.element-card:not(:last-child)::after {
|
|
background: linear-gradient(90deg, transparent, #4fc3f7, transparent);
|
|
}
|
|
|
|
.field-label {
|
|
color: #64b5f6;
|
|
}
|
|
|
|
.field-value {
|
|
color: #e0e0e0;
|
|
}
|
|
|
|
.element-index {
|
|
background: #4fc3f7;
|
|
}
|
|
|
|
.element-name {
|
|
color: #4fc3f7;
|
|
}
|
|
|
|
.element-path {
|
|
color: #999;
|
|
}
|
|
|
|
.attribute-name {
|
|
color: #999;
|
|
}
|
|
|
|
.attribute-value {
|
|
color: #e0e0e0;
|
|
}
|
|
|
|
.element-xml {
|
|
background: #2d2d2d;
|
|
border-color: #555;
|
|
color: #e0e0e0;
|
|
}
|
|
|
|
/* Dark mode formatting */
|
|
code, kbd, samp, var, .code {
|
|
background-color: #3d3d3d;
|
|
color: #e0e0e0;
|
|
}
|
|
|
|
kbd {
|
|
background-color: #1a1a1a;
|
|
color: #fff;
|
|
border-color: #555;
|
|
}
|
|
|
|
samp {
|
|
background-color: #1a4d1a;
|
|
color: #90ee90;
|
|
}
|
|
|
|
var {
|
|
background-color: #4d3d1a;
|
|
color: #ffd700;
|
|
}
|
|
|
|
mark, .highlight {
|
|
background-color: #665500;
|
|
color: #ffeb3b;
|
|
}
|
|
|
|
.critical {
|
|
color: #ff6b6b;
|
|
background-color: #4d1f1f;
|
|
}
|
|
|
|
.optional {
|
|
color: #999;
|
|
}
|
|
|
|
.deprecated {
|
|
color: #888;
|
|
}
|
|
|
|
.todo {
|
|
background-color: #4d3d1f;
|
|
color: #ffb74d;
|
|
}
|
|
|
|
.value {
|
|
color: #64b5f6;
|
|
}
|
|
|
|
.term {
|
|
color: #ba68c8;
|
|
}
|
|
|
|
.version {
|
|
background-color: #1a2332;
|
|
color: #90caf9;
|
|
}
|
|
|
|
blockquote {
|
|
background-color: #2d2d2d;
|
|
border-left-color: #555;
|
|
color: #ccc;
|
|
}
|
|
|
|
pre {
|
|
background-color: #2d2d2d;
|
|
color: #e0e0e0;
|
|
}
|
|
|
|
.ref, .url {
|
|
color: #64b5f6;
|
|
}
|
|
|
|
.error {
|
|
color: #ff6b6b;
|
|
background-color: #4d1f1f;
|
|
}
|
|
|
|
.info-box {
|
|
background: #1e3a8a;
|
|
}
|
|
|
|
.schema-info {
|
|
background: #2d2d2d;
|
|
}
|
|
|
|
.controls button {
|
|
background-color: #4fc3f7;
|
|
}
|
|
|
|
.controls button:hover {
|
|
background-color: #29b6f6;
|
|
}
|
|
|
|
.controls button:disabled {
|
|
background-color: #555;
|
|
}
|
|
|
|
/* Dark mode banner styling */
|
|
.top-banner {
|
|
background: linear-gradient(135deg, #1e3a8a 0%, #581c87 100%);
|
|
}
|
|
|
|
.bottom-banner {
|
|
background: linear-gradient(135deg, #1e3a8a 0%, #581c87 100%);
|
|
}
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<!-- Fixed top banner -->
|
|
<div class="top-banner">
|
|
XSD-Based XML Viewer
|
|
</div>
|
|
|
|
<!-- Fixed bottom banner -->
|
|
<div class="bottom-banner">
|
|
© 2024 XSD XML Viewer | Version 1.0
|
|
</div>
|
|
|
|
<h1>XSD-Based XML Viewer</h1>
|
|
<p>Load an XSD schema file and an XML file to view formatted content based on the schema structure.</p>
|
|
|
|
<div class="upload-container">
|
|
<div class="upload-area" id="xsd-upload-area">
|
|
<p><strong>Step 1: Load XSD Schema</strong></p>
|
|
<p>Drag and drop your XSD file here, or click to select</p>
|
|
<input type="file" id="xsd-input" accept=".txt" style="display: none;">
|
|
<button onclick="document.getElementById('xsd-input').click()">Choose XSD File</button>
|
|
<div id="xsd-status" class="file-status"></div>
|
|
</div>
|
|
|
|
<div class="upload-area" id="xml-upload-area">
|
|
<p><strong>Step 2: Load XML Data</strong></p>
|
|
<p>Drag and drop your XML file here, or click to select</p>
|
|
<input type="file" id="xml-input" accept=".xml" style="display: none;">
|
|
<button onclick="document.getElementById('xml-input').click()" id="xml-button" disabled>Choose XML File</button>
|
|
<div id="xml-status" class="file-status"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="schema-info" class="schema-info" style="display: none;">
|
|
<h3>Schema Information</h3>
|
|
<div id="schema-details"></div>
|
|
</div>
|
|
|
|
<div class="controls" id="controls" style="display: none;">
|
|
<button id="toggle-view-btn" onclick="toggleXMLView()">Show XML</button>
|
|
<button onclick="clearAll()">Clear All</button>
|
|
</div>
|
|
|
|
<div id="content-container"></div>
|
|
|
|
<script>
|
|
// Global variables
|
|
let xsdDoc = null;
|
|
let xmlDoc = null;
|
|
let showXML = false;
|
|
let elementDefinitions = new Map();
|
|
|
|
// DOM elements
|
|
const xsdUploadArea = document.getElementById('xsd-upload-area');
|
|
const xmlUploadArea = document.getElementById('xml-upload-area');
|
|
const xsdInput = document.getElementById('xsd-input');
|
|
const xmlInput = document.getElementById('xml-input');
|
|
const xmlButton = document.getElementById('xml-button');
|
|
const xsdStatus = document.getElementById('xsd-status');
|
|
const xmlStatus = document.getElementById('xml-status');
|
|
const container = document.getElementById('content-container');
|
|
const controls = document.getElementById('controls');
|
|
const toggleBtn = document.getElementById('toggle-view-btn');
|
|
const schemaInfo = document.getElementById('schema-info');
|
|
const schemaDetails = document.getElementById('schema-details');
|
|
|
|
// XSD Upload handlers
|
|
xsdUploadArea.addEventListener('dragover', (e) => {
|
|
e.preventDefault();
|
|
xsdUploadArea.classList.add('dragover');
|
|
});
|
|
|
|
xsdUploadArea.addEventListener('dragleave', () => {
|
|
xsdUploadArea.classList.remove('dragover');
|
|
});
|
|
|
|
xsdUploadArea.addEventListener('drop', (e) => {
|
|
e.preventDefault();
|
|
xsdUploadArea.classList.remove('dragover');
|
|
const files = e.dataTransfer.files;
|
|
if (files.length > 0) {
|
|
handleXSDFile(files[0]);
|
|
}
|
|
});
|
|
|
|
xsdInput.addEventListener('change', (e) => {
|
|
if (e.target.files.length > 0) {
|
|
handleXSDFile(e.target.files[0]);
|
|
}
|
|
});
|
|
|
|
// XML Upload handlers
|
|
xmlUploadArea.addEventListener('dragover', (e) => {
|
|
e.preventDefault();
|
|
if (xsdDoc) {
|
|
xmlUploadArea.classList.add('dragover');
|
|
}
|
|
});
|
|
|
|
xmlUploadArea.addEventListener('dragleave', () => {
|
|
xmlUploadArea.classList.remove('dragover');
|
|
});
|
|
|
|
xmlUploadArea.addEventListener('drop', (e) => {
|
|
e.preventDefault();
|
|
xmlUploadArea.classList.remove('dragover');
|
|
if (xsdDoc) {
|
|
const files = e.dataTransfer.files;
|
|
if (files.length > 0) {
|
|
handleXMLFile(files[0]);
|
|
}
|
|
}
|
|
});
|
|
|
|
xmlInput.addEventListener('change', (e) => {
|
|
if (e.target.files.length > 0) {
|
|
handleXMLFile(e.target.files[0]);
|
|
}
|
|
});
|
|
|
|
function handleXSDFile(file) {
|
|
if (!file.name.toLowerCase().endsWith('.txt')) {
|
|
showError('Please select an XSD file.', xsdStatus);
|
|
return;
|
|
}
|
|
|
|
const reader = new FileReader();
|
|
reader.onload = (e) => {
|
|
try {
|
|
parseXSD(e.target.result);
|
|
xsdStatus.textContent = `✓ ${file.name} loaded`;
|
|
xsdStatus.className = 'file-status loaded';
|
|
xsdUploadArea.classList.add('has-file');
|
|
|
|
// Enable XML upload
|
|
xmlButton.disabled = false;
|
|
xmlStatus.textContent = 'Ready for XML file';
|
|
} catch (error) {
|
|
showError('Error parsing XSD file: ' + error.message, xsdStatus);
|
|
xsdUploadArea.classList.remove('has-file');
|
|
}
|
|
};
|
|
reader.readAsText(file);
|
|
}
|
|
|
|
function handleXMLFile(file) {
|
|
if (!file.name.toLowerCase().endsWith('.xml')) {
|
|
showError('Please select an XML file.', xmlStatus);
|
|
return;
|
|
}
|
|
|
|
if (!xsdDoc) {
|
|
showError('Please load an XSD file first.', xmlStatus);
|
|
return;
|
|
}
|
|
|
|
const reader = new FileReader();
|
|
reader.onload = (e) => {
|
|
try {
|
|
parseXML(e.target.result);
|
|
xmlStatus.textContent = `✓ ${file.name} loaded`;
|
|
xmlStatus.className = 'file-status loaded';
|
|
xmlUploadArea.classList.add('has-file');
|
|
} catch (error) {
|
|
showError('Error parsing XML file: ' + error.message, xmlStatus);
|
|
xmlUploadArea.classList.remove('has-file');
|
|
}
|
|
};
|
|
reader.readAsText(file);
|
|
}
|
|
|
|
function parseXSD(xsdString) {
|
|
const parser = new DOMParser();
|
|
xsdDoc = parser.parseFromString(xsdString, 'text/xml');
|
|
|
|
// Check for parsing errors
|
|
const parseError = xsdDoc.querySelector('parsererror');
|
|
if (parseError) {
|
|
throw new Error('XSD parsing error: ' + parseError.textContent);
|
|
}
|
|
|
|
// Extract element definitions from XSD
|
|
extractElementDefinitions();
|
|
displaySchemaInfo();
|
|
}
|
|
|
|
function extractElementDefinitions() {
|
|
elementDefinitions.clear();
|
|
|
|
// Find all element definitions in the XSD
|
|
const elements = xsdDoc.querySelectorAll('element, xs\\:element, xsd\\:element');
|
|
|
|
elements.forEach(element => {
|
|
const name = element.getAttribute('name');
|
|
const type = element.getAttribute('type');
|
|
|
|
if (name) {
|
|
const definition = {
|
|
name: name,
|
|
type: type || 'string',
|
|
minOccurs: element.getAttribute('minOccurs') || '1',
|
|
maxOccurs: element.getAttribute('maxOccurs') || '1',
|
|
documentation: extractDocumentation(element),
|
|
attributes: extractAttributes(element),
|
|
children: extractChildElements(element)
|
|
};
|
|
|
|
elementDefinitions.set(name, definition);
|
|
}
|
|
});
|
|
|
|
// Also extract complex types
|
|
const complexTypes = xsdDoc.querySelectorAll('complexType, xs\\:complexType, xsd\\:complexType');
|
|
complexTypes.forEach(complexType => {
|
|
const name = complexType.getAttribute('name');
|
|
if (name) {
|
|
const definition = {
|
|
name: name,
|
|
type: 'complex',
|
|
attributes: extractAttributes(complexType),
|
|
children: extractChildElements(complexType)
|
|
};
|
|
elementDefinitions.set(name, definition);
|
|
}
|
|
});
|
|
}
|
|
|
|
function extractDocumentation(element) {
|
|
const doc = element.querySelector('documentation, xs\\:documentation, xsd\\:documentation');
|
|
return doc ? doc.textContent.trim() : '';
|
|
}
|
|
|
|
function extractAttributes(element) {
|
|
const attributes = [];
|
|
const attrElements = element.querySelectorAll('attribute, xs\\:attribute, xsd\\:attribute');
|
|
|
|
attrElements.forEach(attr => {
|
|
attributes.push({
|
|
name: attr.getAttribute('name'),
|
|
type: attr.getAttribute('type') || 'string',
|
|
use: attr.getAttribute('use') || 'optional'
|
|
});
|
|
});
|
|
|
|
return attributes;
|
|
}
|
|
|
|
function extractChildElements(element) {
|
|
const children = [];
|
|
const sequences = element.querySelectorAll('sequence, xs\\:sequence, xsd\\:sequence');
|
|
|
|
sequences.forEach(seq => {
|
|
const childElements = seq.querySelectorAll('element, xs\\:element, xsd\\:element');
|
|
childElements.forEach(child => {
|
|
children.push({
|
|
name: child.getAttribute('name'),
|
|
type: child.getAttribute('type') || 'string',
|
|
minOccurs: child.getAttribute('minOccurs') || '1',
|
|
maxOccurs: child.getAttribute('maxOccurs') || '1'
|
|
});
|
|
});
|
|
});
|
|
|
|
return children;
|
|
}
|
|
|
|
function displaySchemaInfo() {
|
|
schemaInfo.style.display = 'block';
|
|
|
|
const rootElements = Array.from(elementDefinitions.entries())
|
|
.filter(([name, def]) => def.type !== 'complex')
|
|
.map(([name]) => name);
|
|
|
|
schemaDetails.innerHTML = `
|
|
<p><strong>Elements defined:</strong> ${elementDefinitions.size}</p>
|
|
<p><strong>Root elements:</strong> ${rootElements.join(', ') || 'None identified'}</p>
|
|
`;
|
|
}
|
|
|
|
function parseXML(xmlString) {
|
|
const parser = new DOMParser();
|
|
xmlDoc = parser.parseFromString(xmlString, 'text/xml');
|
|
|
|
// Check for parsing errors
|
|
const parseError = xmlDoc.querySelector('parsererror');
|
|
if (parseError) {
|
|
throw new Error('XML parsing error: ' + parseError.textContent);
|
|
}
|
|
|
|
displayXMLContent();
|
|
controls.style.display = 'block';
|
|
}
|
|
|
|
function displayXMLContent() {
|
|
container.innerHTML = '';
|
|
|
|
// Find the root element
|
|
const root = xmlDoc.documentElement;
|
|
|
|
// Find the first-level repeating element type (e.g., "Rule" in software-rules)
|
|
const topLevelElements = findTopLevelRepeatingElements(root);
|
|
|
|
if (topLevelElements.length > 0) {
|
|
// Group by the repeating element type
|
|
const elementGroups = new Map();
|
|
|
|
topLevelElements.forEach(element => {
|
|
const tagName = element.tagName;
|
|
if (!elementGroups.has(tagName)) {
|
|
elementGroups.set(tagName, []);
|
|
}
|
|
elementGroups.get(tagName).push(element);
|
|
});
|
|
|
|
// Display each group
|
|
elementGroups.forEach((elements, tagName) => {
|
|
displayElementGroup(tagName, elements);
|
|
});
|
|
} else {
|
|
// Fallback to original processing if no repeating pattern found
|
|
const processedElements = new Set();
|
|
processElement(xmlDoc.documentElement, '', processedElements);
|
|
}
|
|
}
|
|
|
|
function findTopLevelRepeatingElements(root) {
|
|
// Get all direct children of root
|
|
const children = Array.from(root.children);
|
|
|
|
// Find which tag names appear multiple times
|
|
const tagCounts = new Map();
|
|
children.forEach(child => {
|
|
const tag = child.tagName;
|
|
tagCounts.set(tag, (tagCounts.get(tag) || 0) + 1);
|
|
});
|
|
|
|
// Find the most common repeating element
|
|
let maxCount = 0;
|
|
let repeatingTag = null;
|
|
tagCounts.forEach((count, tag) => {
|
|
if (count > maxCount) {
|
|
maxCount = count;
|
|
repeatingTag = tag;
|
|
}
|
|
});
|
|
|
|
// Return all elements with the repeating tag
|
|
if (repeatingTag && maxCount > 1) {
|
|
return children.filter(child => child.tagName === repeatingTag);
|
|
}
|
|
|
|
// If only single instances, return all children
|
|
return children;
|
|
}
|
|
|
|
function displayElementGroup(tagName, elements) {
|
|
// Create group container
|
|
const groupDiv = document.createElement('div');
|
|
groupDiv.className = 'group-container';
|
|
|
|
// Add group header
|
|
const headerDiv = document.createElement('div');
|
|
headerDiv.className = 'group-header';
|
|
headerDiv.textContent = `${tagName} Elements (${elements.length})`;
|
|
groupDiv.appendChild(headerDiv);
|
|
|
|
// Display each element in compact format
|
|
elements.forEach((element, index) => {
|
|
displayCompactElement(element, index + 1, groupDiv);
|
|
});
|
|
|
|
container.appendChild(groupDiv);
|
|
}
|
|
|
|
function displayCompactElement(element, index, parentContainer) {
|
|
// Create element pair container
|
|
const pairDiv = document.createElement('div');
|
|
pairDiv.className = 'element-pair';
|
|
|
|
// Create formatted element card
|
|
const cardDiv = document.createElement('div');
|
|
cardDiv.className = 'element-card';
|
|
|
|
// Add index badge
|
|
const indexDiv = document.createElement('div');
|
|
indexDiv.className = 'element-index';
|
|
indexDiv.textContent = `#${index}`;
|
|
cardDiv.appendChild(indexDiv);
|
|
|
|
// Process all child elements as fields
|
|
const fields = extractElementFields(element);
|
|
fields.forEach(field => {
|
|
const rowDiv = document.createElement('div');
|
|
rowDiv.className = 'field-row';
|
|
|
|
const labelDiv = document.createElement('div');
|
|
labelDiv.className = 'field-label';
|
|
labelDiv.textContent = formatFieldName(field.name) + ':';
|
|
|
|
const valueDiv = document.createElement('div');
|
|
valueDiv.className = 'field-value';
|
|
valueDiv.innerHTML = field.value;
|
|
|
|
rowDiv.appendChild(labelDiv);
|
|
rowDiv.appendChild(valueDiv);
|
|
cardDiv.appendChild(rowDiv);
|
|
});
|
|
|
|
// Add attributes as fields if present
|
|
if (element.attributes.length > 0) {
|
|
Array.from(element.attributes).forEach(attr => {
|
|
const rowDiv = document.createElement('div');
|
|
rowDiv.className = 'field-row';
|
|
|
|
const labelDiv = document.createElement('div');
|
|
labelDiv.className = 'field-label';
|
|
labelDiv.textContent = `@${attr.name}:`;
|
|
|
|
const valueDiv = document.createElement('div');
|
|
valueDiv.className = 'field-value';
|
|
valueDiv.textContent = attr.value;
|
|
|
|
rowDiv.appendChild(labelDiv);
|
|
rowDiv.appendChild(valueDiv);
|
|
cardDiv.appendChild(rowDiv);
|
|
});
|
|
}
|
|
|
|
// Add show XML button
|
|
if (showXML) {
|
|
const xmlDiv = document.createElement('div');
|
|
xmlDiv.className = 'element-xml';
|
|
xmlDiv.textContent = formatXMLElement(element);
|
|
cardDiv.appendChild(xmlDiv);
|
|
pairDiv.classList.add('show-xml');
|
|
}
|
|
|
|
pairDiv.appendChild(cardDiv);
|
|
parentContainer.appendChild(pairDiv);
|
|
}
|
|
|
|
function extractElementFields(element) {
|
|
const fields = [];
|
|
const processedTags = new Set();
|
|
|
|
// Process each child element as a field
|
|
Array.from(element.children).forEach(child => {
|
|
const fieldName = child.tagName;
|
|
|
|
// Skip if we've already processed this tag
|
|
if (processedTags.has(fieldName)) {
|
|
return;
|
|
}
|
|
|
|
// Check if this is a multi-value field (appears multiple times)
|
|
const siblings = Array.from(element.children).filter(c => c.tagName === fieldName);
|
|
|
|
if (siblings.length > 1) {
|
|
// For multi-value fields, combine them
|
|
const values = siblings.map(s => formatElementContent(s)).filter(v => v && v !== '<em>(empty)</em>');
|
|
if (values.length > 0) {
|
|
fields.push({
|
|
name: fieldName,
|
|
value: values.join(', ')
|
|
});
|
|
}
|
|
processedTags.add(fieldName);
|
|
} else {
|
|
const fieldValue = formatElementContent(child);
|
|
if (fieldValue && fieldValue !== '<em>(empty)</em>') {
|
|
fields.push({
|
|
name: fieldName,
|
|
value: fieldValue
|
|
});
|
|
}
|
|
processedTags.add(fieldName);
|
|
}
|
|
});
|
|
|
|
return fields;
|
|
}
|
|
|
|
function formatFieldName(name) {
|
|
// Convert camelCase or PascalCase to Title Case
|
|
return name
|
|
.replace(/([A-Z])/g, ' $1')
|
|
.replace(/^./, str => str.toUpperCase())
|
|
.trim();
|
|
}
|
|
|
|
function processElement(element, path, processedElements) {
|
|
if (!element || element.nodeType !== Node.ELEMENT_NODE) return;
|
|
|
|
const elementPath = path ? `${path}/${element.tagName}` : element.tagName;
|
|
|
|
// Get all elements with this tag name at this level
|
|
const siblings = element.parentNode ?
|
|
Array.from(element.parentNode.children).filter(child => child.tagName === element.tagName) :
|
|
[element];
|
|
|
|
siblings.forEach((elem, index) => {
|
|
const uniquePath = siblings.length > 1 ? `${elementPath}[${index + 1}]` : elementPath;
|
|
|
|
// Check if this element has text content or attributes worth displaying
|
|
const hasContent = hasDisplayableContent(elem);
|
|
|
|
if (hasContent) {
|
|
displayElement(elem, uniquePath);
|
|
}
|
|
|
|
// Process child elements
|
|
Array.from(elem.children).forEach(child => {
|
|
processElement(child, uniquePath, processedElements);
|
|
});
|
|
});
|
|
}
|
|
|
|
function hasDisplayableContent(element) {
|
|
// Check if element has direct text content
|
|
const textNodes = Array.from(element.childNodes)
|
|
.filter(node => node.nodeType === Node.TEXT_NODE)
|
|
.map(node => node.textContent.trim())
|
|
.join('');
|
|
|
|
// Check if element has attributes
|
|
const hasAttributes = element.attributes.length > 0;
|
|
|
|
// Check if element contains formatted content (any child elements that might be formatting)
|
|
const hasFormattingChildren = Array.from(element.children).some(child => {
|
|
const tagName = child.tagName.toLowerCase();
|
|
return ['strong', 'em', 'code', 'kbd', 'critical', 'value', 'unit', 'term',
|
|
'sup', 'sub', 'mark', 'todo', 'optional', 'deprecated', 'version',
|
|
'ref', 'url', 'br', 'hr', 'ul', 'ol', 'li', 'blockquote', 'pre'].includes(tagName);
|
|
});
|
|
|
|
return textNodes.length > 0 || hasAttributes || hasFormattingChildren;
|
|
}
|
|
|
|
function displayElement(element, path) {
|
|
// Create element pair container
|
|
const pairDiv = document.createElement('div');
|
|
pairDiv.className = showXML ? 'element-pair' : 'element-pair xml-hidden';
|
|
|
|
// Create formatted element card
|
|
const cardDiv = document.createElement('div');
|
|
cardDiv.className = 'element-card';
|
|
|
|
// Add element path
|
|
const pathDiv = document.createElement('div');
|
|
pathDiv.className = 'element-path';
|
|
pathDiv.textContent = path;
|
|
cardDiv.appendChild(pathDiv);
|
|
|
|
// Add element name
|
|
const nameDiv = document.createElement('div');
|
|
nameDiv.className = 'element-name';
|
|
nameDiv.textContent = element.tagName;
|
|
cardDiv.appendChild(nameDiv);
|
|
|
|
// Add element content with formatting
|
|
const contentDiv = document.createElement('div');
|
|
contentDiv.className = 'element-content';
|
|
contentDiv.innerHTML = formatElementContent(element);
|
|
cardDiv.appendChild(contentDiv);
|
|
|
|
// Add attributes if present
|
|
if (element.attributes.length > 0) {
|
|
const attrDiv = document.createElement('div');
|
|
attrDiv.className = 'attribute-list';
|
|
|
|
Array.from(element.attributes).forEach(attr => {
|
|
const itemDiv = document.createElement('div');
|
|
itemDiv.className = 'attribute-item';
|
|
|
|
const nameSpan = document.createElement('span');
|
|
nameSpan.className = 'attribute-name';
|
|
nameSpan.textContent = attr.name + ':';
|
|
|
|
const valueSpan = document.createElement('span');
|
|
valueSpan.className = 'attribute-value';
|
|
valueSpan.textContent = attr.value;
|
|
|
|
itemDiv.appendChild(nameSpan);
|
|
itemDiv.appendChild(valueSpan);
|
|
attrDiv.appendChild(itemDiv);
|
|
});
|
|
|
|
cardDiv.appendChild(attrDiv);
|
|
}
|
|
|
|
// Create XML display div
|
|
const xmlDiv = document.createElement('div');
|
|
xmlDiv.className = 'element-xml';
|
|
xmlDiv.textContent = formatXMLElement(element);
|
|
|
|
pairDiv.appendChild(cardDiv);
|
|
pairDiv.appendChild(xmlDiv);
|
|
container.appendChild(pairDiv);
|
|
}
|
|
|
|
function formatElementContent(element) {
|
|
let html = '';
|
|
|
|
element.childNodes.forEach(node => {
|
|
if (node.nodeType === Node.TEXT_NODE) {
|
|
html += node.textContent;
|
|
} else if (node.nodeType === Node.ELEMENT_NODE) {
|
|
// Format known tags
|
|
const tagName = node.tagName.toLowerCase();
|
|
const content = formatElementContent(node);
|
|
|
|
// Apply appropriate formatting based on tag
|
|
switch(tagName) {
|
|
case 'strong':
|
|
case 'b':
|
|
html += `<strong>${content}</strong>`;
|
|
break;
|
|
case 'em':
|
|
case 'i':
|
|
html += `<em>${content}</em>`;
|
|
break;
|
|
case 'u':
|
|
html += `<u>${content}</u>`;
|
|
break;
|
|
case 'del':
|
|
case 'strike':
|
|
html += `<del>${content}</del>`;
|
|
break;
|
|
case 'sup':
|
|
html += `<sup>${content}</sup>`;
|
|
break;
|
|
case 'sub':
|
|
html += `<sub>${content}</sub>`;
|
|
break;
|
|
case 'mark':
|
|
case 'highlight':
|
|
html += `<mark>${content}</mark>`;
|
|
break;
|
|
case 'small':
|
|
html += `<small>${content}</small>`;
|
|
break;
|
|
case 'code':
|
|
html += `<code>${content}</code>`;
|
|
break;
|
|
case 'kbd':
|
|
html += `<kbd>${content}</kbd>`;
|
|
break;
|
|
case 'samp':
|
|
html += `<samp>${content}</samp>`;
|
|
break;
|
|
case 'var':
|
|
html += `<var>${content}</var>`;
|
|
break;
|
|
case 'pre':
|
|
html += `<pre>${content}</pre>`;
|
|
break;
|
|
case 'critical':
|
|
html += `<span class="critical">${content}</span>`;
|
|
break;
|
|
case 'optional':
|
|
html += `<span class="optional">${content}</span>`;
|
|
break;
|
|
case 'deprecated':
|
|
html += `<span class="deprecated">${content}</span>`;
|
|
break;
|
|
case 'todo':
|
|
html += `<span class="todo">${content}</span>`;
|
|
break;
|
|
case 'value':
|
|
html += `<span class="value">${content}</span>`;
|
|
break;
|
|
case 'unit':
|
|
html += `<span class="unit">${content}</span>`;
|
|
break;
|
|
case 'term':
|
|
html += `<span class="term">${content}</span>`;
|
|
break;
|
|
case 'acronym':
|
|
html += `<span class="acronym">${content}</span>`;
|
|
break;
|
|
case 'version':
|
|
html += `<span class="version">${content}</span>`;
|
|
break;
|
|
case 'br':
|
|
html += '<br>';
|
|
break;
|
|
case 'hr':
|
|
html += '<hr>';
|
|
break;
|
|
case 'ul':
|
|
html += `<ul>${content}</ul>`;
|
|
break;
|
|
case 'ol':
|
|
html += `<ol>${content}</ol>`;
|
|
break;
|
|
case 'li':
|
|
html += `<li>${content}</li>`;
|
|
break;
|
|
case 'blockquote':
|
|
html += `<blockquote>${content}</blockquote>`;
|
|
break;
|
|
case 'ref':
|
|
html += `<span class="ref">${content}</span>`;
|
|
break;
|
|
case 'url':
|
|
html += `<span class="url">${content}</span>`;
|
|
break;
|
|
case 'link':
|
|
const href = node.getAttribute('href') || '#';
|
|
html += `<a href="${href}" class="url">${content}</a>`;
|
|
break;
|
|
default:
|
|
// For unknown tags, just include the content
|
|
html += content;
|
|
}
|
|
}
|
|
});
|
|
|
|
return html || '<em>(empty)</em>';
|
|
}
|
|
|
|
function formatXMLElement(element) {
|
|
const serializer = new XMLSerializer();
|
|
const xmlString = serializer.serializeToString(element);
|
|
|
|
// Basic formatting with indentation
|
|
return xmlString
|
|
.replace(/></g, '>\n<')
|
|
.split('\n')
|
|
.map((line, index) => {
|
|
const indent = ' '.repeat(Math.min(index, 4));
|
|
return indent + line;
|
|
})
|
|
.join('\n');
|
|
}
|
|
|
|
function toggleXMLView() {
|
|
showXML = !showXML;
|
|
|
|
// Refresh the display with the new setting
|
|
displayXMLContent();
|
|
|
|
toggleBtn.textContent = showXML ? 'Hide XML' : 'Show XML';
|
|
}
|
|
|
|
function clearAll() {
|
|
xsdDoc = null;
|
|
xmlDoc = null;
|
|
elementDefinitions.clear();
|
|
container.innerHTML = '';
|
|
controls.style.display = 'none';
|
|
schemaInfo.style.display = 'none';
|
|
|
|
xsdStatus.textContent = '';
|
|
xsdStatus.className = 'file-status';
|
|
xsdUploadArea.classList.remove('has-file');
|
|
|
|
xmlStatus.textContent = '';
|
|
xmlStatus.className = 'file-status';
|
|
xmlUploadArea.classList.remove('has-file');
|
|
xmlButton.disabled = true;
|
|
|
|
xsdInput.value = '';
|
|
xmlInput.value = '';
|
|
}
|
|
|
|
function showError(message, statusElement) {
|
|
if (statusElement) {
|
|
statusElement.textContent = message;
|
|
statusElement.className = 'file-status error';
|
|
} else {
|
|
container.innerHTML = `<div class="error">${message}</div>`;
|
|
}
|
|
}
|
|
|
|
// Display initial info
|
|
container.innerHTML = `
|
|
<div class="info-box">
|
|
<h3>How to use this viewer:</h3>
|
|
<ol>
|
|
<li>Load an XSD schema file first - this defines the structure of your XML</li>
|
|
<li>Load an XML file that conforms to the schema</li>
|
|
<li>All text content will be displayed with HTML formatting support</li>
|
|
</ol>
|
|
<p><strong>Supported formatting tags:</strong> strong, em, code, kbd, critical, optional, todo, value, unit, term, version, and more!</p>
|
|
</div>
|
|
`;
|
|
</script>
|
|
</body>
|
|
</html> |