本文将介绍利用xml与xsl技术内容与表示相分离的特点,创建一个易于扩充,结构灵活的用户手册文档。当然,本文的重点在于阐述一种利用xml的方法,并不仅限于用户手册文档,您可以利用在任何您认为需要的地方。
本文介绍的内容基于xml、xsl与dtd技术规范,您可以使用ie5.0及其以上版本进行测试。
用户的需求
从用户的视角,我们要实现的用户手册包含以下几个部分:
在页面左侧的导航栏,利用树状显示每一个级别的标题,每个标题前按顺序标有标题序号,形如2.1或2.1.8,对于包含子标题的标题要与不包含子标题的标题,即叶子标题区分显示,前面用不同的图片区分。
页面右侧是内容栏,除包含各级标题外,还包含各级标题下的具体内容,内容类型有文字和图片。不同级别的标题要使用不同的现实方式,每个标题前也加有标题序号,与左侧导航栏一致。
对于含有图片的内容部分,图片显示为缩略图,通过点击可以在正常大小与缩略图之间切换。
点击左侧导航栏的标题,可以定位到右侧的相关内容。
对于手册的标题和内容,应该可以方便添加与删除,修改了一个标题的级别,比如从一级标题换到三级标题的位置,其显示风格可以自动更改。理论上,标题的级别可能会有无限多层。
无论是左侧的导航栏,还是右侧的内容区域,用户只对标题的相对位置和级别负责,标题前面的序号要求自动生成。
对应的技术实现
我们打算开发如下几个部分来实现这个用户手册:
一个dtd定义文件,即document type defination(文档类型定义)。定义这个文件需要根据客户对于手册的需求来做,这也是我们需要首先着手去做的部分。
一个xml文档,包含了用户手册的真实的、具体的内容,这个文档当然应该是良构的,需要遵循我们在上面dtd文件中定义的规则。这个文档的内容由手册的编写者来提供,他无需了解其他的技术细节,只需遵照一个简单的规则即可,如果能够使用像xmlspy这样的工具编辑会更省力。
两个xsl文件,这是我们需要开发的主体部分,也就是决定内容将如何呈现给用户,共有两个xsl文件,分别针对左侧的导航栏和右侧的内容主体。
一个超文本文件,构成用户手册的框架,实现导航栏与内容主体的分割。
定义dtd
通过前面的需求陈述,可以抽象出如下页面元素:
标题,会有不同的级别
内容,包括文本和图片
为此我们定义如下dtd元素:
<!element image (#pcdata)>
<!element para (#pcdata)>
<!element name (#pcdata)>
分别表示图片、段落文本和标题。为了表示标题的嵌套关系,我们又定义一个item元素,把它作为一个块,不同的item之间可以是平行关系,也可以是嵌套关系,所有的基本元素(图片、段落文本和标题)必须从属于一个item。具体的关系描述如下:
一个item中只能包含、并且必须包含一个标题(name)
一个item中可以同时包含多段文字或者多张图片,或者只包含其中一种
一个item中可以嵌套一个或多个item,也可以不嵌套任何item,嵌套的item具有相同的属性
通过以上描述,我们可以定义元素item:
<!element item (name, (para | image)*, item*)>
元素名称后面的* 代表此元素可以为0个或者多个。
(para | image)* 与 para*, image* 同义。
作为dtd,还必须定义一个根元素,我们起名为pms_help,它将包含item元素,进而间接的包含了所有的元素。
<!element pms_help (item*)>
上面表示为,使用了这个dtd的xml文档中可以定义0个或者多个item元素。
此外,我们还需要为图片的缩略图定义:
<!attlist image small id #implied>
至此,dtd文件我们就已经定义完了,我们将其保存为pms_help.dtd,这里下载完整的dtd文件。
如何准备手册内容
接下来我们要根据上面定义的dtd文件,整理含有实际内容的xml文件,文件内容示例如下:
<pms_help>
<item>
<name>第一级别标题</name>
<item>
<name>第二级别标题</name>
<item>
<name>第三级别标题</name>
<para>段落文本1</para>
<image small="image1_small.gif">image1.gif</image>
<para>段落文本2</para>
<para>段落文本3</para>
</item>
<item>
…
</item>
</item>
<item>
…
</item>
</item>
<item>
…
</item>
</pms_help>
在第三级标题下面,当然还可以继续嵌套item元素,有更多级别的标题,只要你在下面的xsl样式文件中对应说明了这个级别标题如何显示即可。
在para标记之间的文本在显示时会被格式化为一个段落,如果你有多段文本,应该分别用para进行标记。
image标记中有一个属性字段small,其值应是一个图片文件名,这个图片用作默认的缩略图显示,当点击这个图片,会显示出image标记之间的图片文件,当然您也可以对着两个图片使用同一个文件名。图片文件的路径不需要在这里指定,路径是写在下面xsl文件中的。
当我们把包含手册内容的xml文件准备好之后,我们还需要做一个额外的工作,就是将此xml文件复制一份,两个文件分别取名为:pms_help.xml, pms_help_lf.xml,两个文件分别绑定不同的xsl样式文件,以取得不同的显示效果,除此以外,两个xml文件是完全相同的。下载两个xml文件:导航栏xml文件,内容栏xml文件
最关键的部分: 开发xsl
1. 首先实现相对简单一点的对应左侧导航栏的xsl文件。
xml文件以一定的结构描述了我们要展现的内容,而在xsl当中,决定了这些内容将以何种方式展现,实际上是xsl是将xml文档格式化为浏览器可以理解的html格式文本,在xsl中首先需要定义根级的模板,实际对应了html文档的head以及body部分,代码如下:
<xsl:template match="pms_help">
<html>
<head>
<title>index</title>
<style media="screen">
<xsl:comment><![cdata[
body {font-fimaly:宋体,arial; font-size:9pt; color:#000000;}
]]></xsl:comment>
</style>
</head>
<body>
<h2>index</h2>
<p/>
<xsl:for-each select="item">
<xsl:apply-templates select="."/>
</xsl:for-each>
</body>
</html>
</xsl:template>
接下来实现对应处理item部分的模板:
<xsl:variable name="allparentnode" select="ancestor::item/name"/>
<xsl:variable name="allchildnode" select="child::item/name"/>
<xsl:variable name="numofallparentnode" select="count($allparentnode) + 1"/>
<xsl:variable name="numofallchildnode" select="count($allchildnode)"/>
<!– output the index of help begin –>
<xsl:element name="div">
<xsl:attribute name="onclick">
window.open(pms_help.xml#<xsl:number count="item" level="multiple" format="01-01-01"/>,main)
<!– <xsl:value-of select=""alert(this.id)"" /> –></xsl:attribute>
<xsl:attribute name="style"><xsl:value-of select="concat(text-indent:,(($numofallparentnode – 1) * 30),;white-space: nowrap)"/></xsl:attribute>
<xsl:attribute name="id"><xsl:number count="item" level="multiple" format="01-01-01"/></xsl:attribute>
<xsl:attribute name="onmouseover"><xsl:value-of select="" this.style.color=red;this.style.cursor=hand ""/></xsl:attribute>
<xsl:attribute name="onmouseout"><xsl:value-of select="" this.style.color=black ""/></xsl:attribute>
<xsl:choose>
<xsl:when test="$numofallparentnode = 0">
<img src="images/page/minus.gif" alt="" border="0"/>
</xsl:when>
<xsl:when test="$numofallparentnode != 0 and $numofallchildnode = 0">
<img src="images/page/passage.gif" alt="" border="0"/>
</xsl:when>
<xsl:otherwise>
<img src="images/page/minus.gif" alt="" border="0"/>
</xsl:otherwise>
</xsl:choose>
<xsl:number count="item" level="multiple"/>_
<xsl:value-of select="name"/>
</xsl:element>
<!– 如果还存在下一级item,则递归调用此模板 –>
<xsl:if test="count(item)>0">
<xsl:apply-templates select="item"/>
</xsl:if>
</xsl:template>
在上面的代码中,我们通过如下的方式定义变量:
<xsl:variable name="allparentnode" select="ancestor::item/name"/>
其中,allparentnode是我们为变量取的名字,后面使用select关键字为其赋值,ancestor关键字用于取出后面给定节点的所有父节点,item/name是我们在dtd中定义的节点名称。之后我们可以通过 $allparentnode 来引用这个变量。接下来我们使用:
<xsl:variable name="numofallparentnode"
select="count($allparentnode) + 1"/>
取出当前节点所有父节点的数量。使用以下代码能够在xsl当中实现一个类似case语句的功能,用来判断是否是叶子节点,以显示不同的图片。
<xsl:choose>
<xsl:when test="$numofallparentnode = 0">
<img src="images/page/minus.gif" alt="" border="0"/>
</xsl:when>
<xsl:when test="$numofallparentnode != 0 and $numofallchildnode = 0">
<img src="images/page/passage.gif" alt="" border="0"/>
</xsl:when>
<xsl:otherwise>
<img src="images/page/minus.gif" alt="" border="0"/>
</xsl:otherwise>
</xsl:choose>
由于我们的dtd中不限制item定义的级别,因此,我们需要递归的处理item的显示,使用如下语句:
<xsl:if test="count(item)>0">
<xsl:apply-templates select="item"/>
</xsl:if>
我们将此xsl文件保存为pms_help_lf.xsl,下载此文件。
2. 接下来实现对应右侧具体内容展示的xsl。
同样,也需要定义head和body部分,不同的是,这里定义了更加复杂的样式单(css),并增加了一段javascript脚本:
<xsl:template match="pms_help">
<html>
<head>
<title>content</title>
<style media="screen, print">
<xsl:comment><![cdata[
body {font-fimaly:宋体,arial; font-size:9pt; color:#000000;}
.title1 {font-fimaly:宋体; font-size:17.2pt; color:#333333; font-weight:bold; margin-left:0pt; background-color:#eeeeee; white-space: nowrap}
.title2 {font-fimaly:宋体; font-size:14pt; color:#000000; font-weight:bold; margin-left:0pt; white-space: nowrap}
.title3 {font-fimaly:宋体; font-size:12pt; color:#333333; font-weight:bold; margin-left:90pt; background-color:#eeeeee; white-space: nowrap}
.title4 {font-fimaly:宋体; font-size:12pt; color:#000000; font-weight:bold; margin-left:90pt; white-space: nowrap}
.title5 {font-fimaly:宋体; font-size:10pt; color:#333333; font-weight:bold; margin-left:160pt; background-color:#eeeeee; white-space: nowrap}
.title6 {font-fimaly:宋体; font-size:10pt; color:#000000; font-weight:bold; margin-left:160pt; white-space: nowrap}
.para1 {font-fimaly:宋体; font-size:9pt; color:#333333; margin-left:90pt}
.para2 {font-fimaly:宋体; font-size:9pt; color:#333333; margin-left:90pt}
.para3 {font-fimaly:宋体; font-size:9pt; color:#333333; margin-left:90pt}
.para4 {font-fimaly:宋体; font-size:9pt; color:#333333; margin-left:90pt}
.para5 {font-fimaly:宋体; font-size:9pt; color:#333333; margin-left:160pt}
.para6 {font-fimaly:宋体; font-size:9pt; color:#333333; margin-left:160pt}
.image {cursor:hand}
]]></xsl:comment>
</style>
<script>
<xsl:comment><![cdata[
/* 当用户点击图片时,交替显示缩略图和全图 */
function swapimg(which, small, normal){
var s = "/";
//alert(which + | + small + | + normal);
//alert(which.src.substring(which.src.lastindexof(s)+1));
if (which.src.substring(which.src.lastindexof(s)+1) == small){
which.src = images/ + normal;
which.alt = ===> 缩小 <===;
}
else{
which.src = images/ + small;
which.alt = <=== 放大 ===>;
}
}
]]></xsl:comment>
</script>
</head>
<body>
<a name="top"/>
<h2>content</h2>
<p align="right">
<a href="mailto:yourname@yourdomain.com">email to us</a><xsl:text> </xsl:text><a href="javascript:print()">打印本手册</a>
</p>
<xsl:for-each select="item">
<xsl:apply-templates select="."/>
</xsl:for-each>
</body>
</html>
</xsl:template>
接下来是处理item的部分:
<xsl:template match="item">
<xsl:variable name="allparentnode" select="ancestor::item/name"/>
<xsl:variable name="allchildnode" select="child::item/name"/>
<xsl:variable name="numofallparentnode" select="count($allparentnode) + 1"/>
<xsl:variable name="numofallchildnode" select="count($allchildnode)"/>
<!– output the content of help –>
<!– 显示各级标题 –>
<xsl:element name="a">
<xsl:attribute name="name"><xsl:number count="item" level="multiple" format="01-01-01"/></xsl:attribute>
</xsl:element>
<xsl:element name="div">
<xsl:attribute name="class"><xsl:value-of select="concat(title, $numofallparentnode)"/></xsl:attribute>
<xsl:number count="item" level="multiple"/><xsl:text> </xsl:text>
<xsl:value-of select="name"/>
<!– 级别为奇数的标题下加横线 –>
<xsl:if test="$numofallparentnode = 1 or ($numofallparentnode mod 2) != 0">
::<a href="#top" target="_self">top</a>
<hr size="1" noshade="noshade"/>
</xsl:if>
</xsl:element>
<p/>
<!– 如果还存在下一级item,则递归调用此模板 –>
<xsl:if test="count(item)>0">
<xsl:apply-templates select="item"/>
</xsl:if>
<!– 如果不存在下一级标题,显示段落和图片内容 –>
<xsl:if test="count(item)=0">
<xsl:apply-templates select="para | image"/>
</xsl:if>
</xsl:template>
为了以不同的样式显示不同级别的标题我们做了如下处理,根据级别的不同,选择不同的样式单(css)定义:
<xsl:attribute name="class"><xsl:value-of select="concat(title, $numofallparentnode)"/></xsl:attribute>
额外的,我们在级别为奇数的标题下画上横线:
<!– 级别为奇数的标题下加横线 –>
<xsl:if test="$numofallparentnode = 1 or ($numofallparentnode mod 2) != 0">
::<a href="#top" target="_self">top</a>
<hr size="1" noshade="noshade"/>
</xsl:if>
同样的,我们也需要递归调用此模板进行处理:
<!– 如果还存在下一级item,则递归调用此模板 –>
<xsl:if test="count(item)>0">
<xsl:apply-templates select="item"/>
</xsl:if>
与上一个xsl不同,我们在这里还需要处理para和image标记,这些内容在导航栏是不需要显示的。我们定义了模板:
<xsl:template match="para | image">
<xsl:variable name="allparentnode" select="ancestor::item/name"/>
<xsl:variable name="numofallparentnode" select="count($allparentnode) + 1"/>
<!– 如果节点是para,显示段落 –>
<xsl:if test="self::para">
<xsl:element name="div">
<xsl:attribute name="class"><xsl:value-of select="concat(para, ($numofallparentnode) – 1)"/></xsl:attribute>
<p>
<xsl:value-of select="."/>
</p>
</xsl:element>
</xsl:if>
<!– 如果节点是image,显示图片 –>
<xsl:if test="self::image">
<xsl:variable name="small" select="normalize-space(@small)"/>
<xsl:variable name="normal" select="normalize-space(.)"/>
<xsl:element name="div">
<xsl:attribute name="class"><xsl:value-of select="concat(para, ($numofallparentnode) – 1)"/></xsl:attribute>
<xsl:element name="img">
<xsl:choose>
<xsl:when test="boolean($small)">
<xsl:attribute name="src">images/<xsl:value-of select="$small"/></xsl:attribute>
<xsl:attribute name="alt"><=== 放大 ===></xsl:attribute>
<xsl:attribute name="onclick"><xsl:value-of select="concat(swapimg(this, ",$small,",",$normal,"))"/></xsl:attribute>
</xsl:when>
<xsl:when test="not(boolean($small))">
<xsl:attribute name="src">images/<xsl:value-of select="$normal"/></xsl:attribute>
</xsl:when>
</xsl:choose>
<xsl:attribute name="class">image</xsl:attribute>
<xsl:attribute name="border">0</xsl:attribute>
</xsl:element>
<p/>
</xsl:element>
</xsl:if>
</xsl:template>
通过onclick属性,我们为图片定义了onclick事件,以处理缩略图与正常大小图片之间的转换:
<xsl:attribute name="onclick"><xsl:value-of select="concat(swapimg(this, ",$small,",",$normal,"))"/></xsl:attribute>
这个xsl文件我们保存为pms_help.xsl,下载此xsl文件。
将各部分粘合起来
最后写一个html文件,定义两个框架,将两个xml文件粘合起来,点此查看此用户手册的效果。
回顾一下,此用户手册包含:
一个dtd文件,用于定义xml文档的结构。此文件在xml文件中被引用,但并不是强制性的,换句话说,你可以根本就不生成这个文件,但实际上你必须遵循一定的规则来编写xml文件以及实现你的xsl样式文件。
两个xml文件,包含了手册的实际内容。这两个文件除了链接的xsl样式文件不同外,其他完全相同。
两个xsl样式文件,在以上的两个xml文件中被引用。一个用于格式化左侧导航栏显示,一个用于格式化右侧具体内容显示。
一个超文本文件,用于粘合两个xml文件。
