Вот пример. Это черновик, который изначально состоит из отдельных файлов, которые я объединил в один для простоты тестирования. В конечном результате этот пример должен быть существенно переработан для публикации.
Я счел нужным дать некоторое пространное описание этого черновика. Если эти объяснения скучны и неинтересны, то сразу переходите к самому главному в конце.
Для отображения имени xml-тега выбран H3 из соображений, что он лучше подходит для отображения структуры (так же как Вы выбрали списки). Дизайнер из меня не очень хороший, но я предпочитаю большие буквы и много пустого пространства на странице. Поэтому не пугайтесь.
-- В CSS предусмотрены практически все необходимые правила (некоторые вставлены как пустышки). Кроме того добавлено немного рюшечек, имитирующих раскрывающиеся/схлопывающиеся подуровни в виде символов плюс/минус.
-- XSL "знает" как преобразовать текст, комментарии, xslt-инструкции, вложенные и одиночные теги и атрибуты тегов. Также "различает" теги с установленным атрибутом type="text/*".
-- JavaScript добавляет необходимую динамику - свернуть/развернуть теги.
-- HTA контейнер всего этого хозяйства.
Еще немного слов по поводу динамики. Она достигалась добавлением класса к внешнему тегу. Сокращенный пример, объясняющий действо.
Обычный вид - тег развернут
<div class="node-container">
<h3 class="node-tag">...</h3>
<div class="node-content">
...
</div>
</div>
.
Тег свернут
<div class="node-container collapsed">
<h3 class="node-tag">...</h3>
<div class="node-content">
...
</div>
</div>
.
Желаемый результат достигается за счет того, что заданы CSS-правила:
.collapsed .node-tag {
background-color: #ffc;
}
.collapsed .node-content {
display: none;
}
Первое правило меняет фоновый цвет тега, второе правило скрывает содержимое. Код управляет добавлением/удалением класса .collapsed элементу .node-container при клике внутри элемента .node-tag.
<html>
<head>
<title>XML Viewer</title>
<!--
<link type="text/css" rel="stylesheet" href="xmlview.css" />
-->
<style type="text/css">
.instruction {
border: 1px solid #00f;
color: #00c;
font-style: italic;
font-size: 11pt;
}
.comment {
border: 1px solid #ccc;
color: #999;
font-style: italic;
font-size: 11pt;
}
.node-container {
border-left: 1px solid #eee;
font-family: Verdana, Tahoma, sans-serif;
font-size: 11pt;
}
.node-single-tag {
margin: 0;
}
.node-tag {
border-bottom: 1px solid #eee;
cursor: pointer;
margin: 0;
}
.node-name {
}
.node-text {
}
.node-content {
margin: 0 0 0 20px;
}
.node-code {
border: 1px solid #ccc;
background-color: #eee;
font-size: 0.8em;
margin: 0;
}
.attr {
font-size: 0.9em;
}
.attr-name {
color: #c00;
font-weight: bold;
}
.attr-value {
color: #00c;
font-style: italic;
}
.collapsed .node-tag {
background-color: #ffc;
}
.collapsed .node-content {
display: none;
}
.sign-closed,
.sign-opened {
font-size: 12pt;
margin-left: -15px;
text-align: center;
width: 15px;
}
.sign-closed {
display: none;
}
.collapsed .sign-closed {
display: inline-block;
}
.sign-opened {
display: inline-block;
}
.collapsed .sign-opened {
display: none;
}
.hidden {
display: none;
}
.error {
background-color: #ffc;
border: 1px solid #f00;
font-family: Verdana, Tahoma, sans-serif;
padding: 10px;
}
.error-line {
color: #f00;
}
</style>
<!--
<script type="text/javascript" src="xmlview.js"></script>
-->
<script type="text/javascript"><!--//--><![CDATA[//><!--
var Element = {
/**
* Checks that an element has a provided className
*/
classExists: function(element, className)
{
return this.RE(className).test(element.className);
},
/**
* Creates the regular expression to check that
* a className is defined for an element and
* remove an existing one from an element
*/
RE: function(className)
{
return new RegExp('(?:^|\\s+)' + className + '(?:\\s+|$)', 'g');
},
/**
* Adds a className to an element
*/
addClass: function(element, className)
{
if ( ! this.classExists(element, className) ) {
element.className += ' ' + className;
}
return element;
},
/**
* Removes a classname
*/
removeClass: function(element, className)
{
className = element.className.replace(this.RE(className), ' ');
if ( className != element.className ) {
element.className = className;
}
return element;
},
/**
* Toggles (turn on/turn off) a className
*/
toggleClass: function(element, className)
{
if ( this.classExists(element, className) ) {
this.removeClass(element, className);
} else {
this.addClass(element, className);
}
return element;
}
};
var XMLView = {
/**
* Loads and validate a xml-file
*/
loadXml: function(filename, async, onreadystatechange)
{
var doc = new ActiveXObject("Msxml2.DOMDocument");
doc.async = async;
if ( typeof onreadystatechange == 'function' ) {
doc.onreadystatechange = onreadystatechange;
}
doc.load(filename);
return doc;
},
/**
* Loads an input xml-file, shows errors if they occur
*
* loader is callback-function that should return an xml-document
*
* onerror is callback function, handler of parsing errors
*/
loadSafely: function(loader, onerror)
{
if ( typeof loader != 'function' ) {
throw new TypeError();
}
var doc = loader(this);
if ( ! doc ) {
return;
}
var err = doc.parseError;
if ( err.errorCode == 0 ) {
return doc;
}
if ( typeof onerror == 'function' ) {
onerror(err);
}
},
/**
* Loads an input xml-file and the predefined xsl-file
* and transforms a xml-file accordingly xsl
*
* xmlLoader and xslLoader are callback-functions that
* should return xml-documents
*
* onerror is callback function, handler of parsing errors
*/
transform: function(xmlLoader, xslLoader, onerror)
{
var xml = this.loadSafely(xmlLoader, onerror);
if ( ! xml ) {
return;
}
var xsl = this.loadSafely(xslLoader, onerror);
if ( ! xsl ) {
return;
}
return xml.transformNode(xsl);
},
/**
* The main routine
*
* Loads the predefined xsl-file
* Loads an input xml-file
* Transform an input file
* Outputs the result or details on error to a html page
*
* element is a reference to a html-element that is provided
* when the method is called
*
* xmlLoader and xslLoader are callback-functions that
* should return well-fromed xml-files
*
* onerror is callback function, handler of parsing errors
*/
init: function(element, xmlLoader, xslLoader, onerror)
{
var html;
if ( element ) {
html = this.transform(xmlLoader, xslLoader, onerror);
}
if ( html ) {
element.innerHTML = html;
}
document.onmousedown = function(e)
{
e = e || event;
var target = e.target || e.srcElement;
// Looking for the nearest node-tag
while ( target && (target.className || '').indexOf('node-tag') == -1 ) {
target = target.parentNode;
}
if ( ! target ) {
return;
}
// Perform toggle (turn on/turn off)
Element.toggleClass(target.parentNode, 'collapsed');
};
}
};
//--><!]]></script>
<script type="text/javascript"><!--//--><![CDATA[//><!--
window.onload = function()
{
var html_entity_encode = function(value)
{
var div = document.createElement('div');
var text = document.createTextNode(value);
div.appendChild(text);
return div.innerHTML.replace(/"/g, '"').replace(/'/g, ''');
};
var xmlfile;
// var xslfile = 'xmlview.xsl';
XMLView.init(
// the reference to the xml-container
document.getElementById('xml-container'),
// xml-file loader
function(XMLView)
{
// Load a file using type=file input
var f = document.getElementById('openFile');
f.click();
xmlfile = f.value;
if ( ! xmlfile ) {
return;
}
document.title += ' :: ' + xmlfile;
return XMLView.loadXml(xmlfile);
},
// xsl-file loader
function(XMLView)
{
return document.getElementById('xslDocument');
// return XMLView.loadXml(xslfile);
},
// parsing error handler
function(err)
{
Element.removeClass(document.getElementById('error-container'), 'hidden');
document.getElementById('error-url').innerHTML = err.url;
document.getElementById('error-reason').innerHTML = err.reason;
document.getElementById('error-line').innerHTML = err.line;
document.getElementById('error-linepos').innerHTML = err.linepos;
document.getElementById('error-srcText').innerHTML = html_entity_encode(err.srcText);
document.getElementById('error-pointer').innerHTML = new Array(err.linepos).join('-') + '^';
}
);
};
//--><!]]></script>
</head>
<body>
<div id="xml-container"></div>
<div class="error hidden" id="error-container">
<b>URL: </b><span id="error-url"></span><br />
<b>Error: </b><span id="error-reason"></span><br />
<b>Line: </b><span id="error-line"></span><br />
<b>Position: </b><span id="error-linepos"></span><br />
<br />
<code class="error-line" id="error-srcText"></code><br />
<code class="error-line" id="error-pointer"></code>
</div>
<div class="hidden">
<input type="file" id="openFile" />
</div>
</body>
<xml id="xslDocument">
<!--
<?xml version="1.0"?>
-->
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="html" version="1.0" indent="no" />
<xsl:template match="/">
<xsl:apply-templates />
</xsl:template>
<xsl:template match="processing-instruction()">
<div class="instruction"><xsl:value-of select="name()"/><xsl:text> </xsl:text><xsl:value-of select="."/></div>
</xsl:template>
<xsl:template match="comment()">
<div class="comment"><xsl:value-of select="." /></div>
</xsl:template>
<xsl:template match="text()">
<span class="node-text"><xsl:value-of select="." /></span>
</xsl:template>
<xsl:template match="*">
<div class="node-container">
<h3 class="node-single-tag"><span class="node-name"><xsl:value-of select="name()" /></span><xsl:apply-templates select="@*" /></h3>
</div>
</xsl:template>
<xsl:template match="*[node()]">
<div class="node-container">
<h3 class="node-tag">
<span class="sign-opened">-</span><span class="sign-closed">+</span>
<span class="node-name"><xsl:value-of select="name()" /></span><xsl:apply-templates select="@*" /></h3>
<div class="node-content">
<xsl:choose>
<xsl:when test="starts-with(@type,'text/')">
<pre class="node-code"><xsl:value-of select="." /></pre>
</xsl:when>
<xsl:otherwise>
<xsl:apply-templates />
</xsl:otherwise>
</xsl:choose>
</div>
</div>
</xsl:template>
<xsl:template match="@*">
<xsl:text> </xsl:text><span class="attr"><code class="attr-name"><xsl:value-of select="name()" /></code><xsl:text>=</xsl:text><code class="attr-value"><xsl:value-of select="." /></code></span>
</xsl:template>
</xsl:stylesheet>
</xml>
</html>
( 2 * b ) || ! ( 2 * b )