我通常使用 HTML 文档来管理信息。这种页面描述语言的使用非常简单,将有用数据存储在 web 页面中非常方便 —— 在堆之间没有闲置的文档和文档丢失,这些情况再也不会出现了。假定更为复杂的 HTML 文档是有序的(<ol>)或无序的(<ul>),通常使用像从 <h1> 到 <h6> 的标题标记(尽管后面的标题标记不经常使用)或列表加以构造。此外,在文档中其他结构性元素之间还可能存在图或表格。通常像章节标题、表格和图这样的元素得到分配给能够引用它们的编号,例如:
......
3. 配置代理服务器
......
3.1 设置服务器
......
3.2 配置代理规则
......
在文本中的某些位置上我们可以找到类似
......
有关设置代理服务器的更多信息,请参见第 3.1 章。
......
使用图和表格的工作原理相同。此时,问题变得明朗了: 如果在现有结构之间插入某些内容,例如一个新的章节 3.1,所有后面的章节标题必须重新编号。除此之外,对章节编号的引用也必须修改。这显然既烦人 —— 更糟糕的是 —— 又容易发生错误,特别是如果此类编号和引用跨越多个构成实际文档的 HTML 文件,这种问题尤为突出。
在您办公套件中的日常文字处理程序提供了另一个宜人的功能: 自动创建目录或图列表和表格列表。在 HTML 中,此类列表必须手动创建和维护。这还不是我们通常乐于做的事情。
本文中描述的 Xref 工具对这些问题提供了一个简单的解决方案以及一些附加功能。除此之外,该工具还是如何高效利用一些 J2SE 5.0 新功能的有用示例。
当然 Xref 本身可以以多种方式简化 HTML 内容的创建,其主要目标是 web 创作区域,其中创建了跨越一个或多个 HTML 文档的文本。该区域在几种复杂性级别上受到支持。
在其应用程序的最简单形式中,使用 HTML 标题标记进行构造的现有文档可以被处理,且标题将被自动编号。这一结果在不进行任何更改的情况下获得。如果需要,带有指向所有章节的所有章节和超链接目录可以通过向文档简单添加一个新标记而生成。这是惟一的必要更改。
在 HTML 文档中,应用程序的更复杂形式要求多项更改: 通过添加新的 HTML 标记,对于其他像图或表格这样的对象可以获得另外的自动编号。再者,还支持列表(图列表或表格列表)的创建
完整的 Xref 功能通过使用引用功能实施。使用另一个新标记 (<ref>)对任何被添加标签对象(例如章节标题、图或表格)的引用可以在文档内创建,且将正确的编号插入在相同位置。除此之外,指向被引用的对象的超链接也将被创建。
所有在本文中描述的源代码和带有编译后的类的 JAR 文件可下载 —— 请参见 参考资料 部分。
尽管许多流行的浏览器还不支持本文中描述的一些功能,但它们还可以在 CSS2 中所指定的计数器基础上实现 (请参见 参考资料 部分)。这些计数器还可以被用于自动编号,例如章节标题的自动编号。在本文描述的工具更加专注于交叉引用任务,并提供除 CSS2 计数器之外的几项功能:
像图和表格(以及其他任意序列)这类项目的编号基于新的伪 HTML 标记,直接插入到文档内。
包括对被编号项目(像章节或图)的引用解析、针对目标的指定锚点的创建和 <a href> 这样的引用在内的自动交叉引用功能。
在文档中,包括对所引用项目的 <a href> 引用在内的交叉引用列表的自动生成。
跨文档功能(编号和引用能跨越多个文档 )。
除此之外,针对 J2SE 5.0 发行版的一些新功能还旨在成为展示应用程序。
针对交叉引用进程的标记
已经对三个新标记加以定义以便达到该工具的目的:
标记 <lab name="myLabel" type="figure">...</lab>:
该标记在随后即被引用的 HTML 文档中设置给定类型的标签。该标签还接受可选的 text 属性,用该属性可以显式设置用于在自动生成列表(默认时,在开始标记和结束 <lab> 标记之间包含的文本被用于这一目的 )中的标签的文本。关于这方面的更多详细信息稍后加以介绍。
标记 <ref name="MainData" type="table">...</ref>:
该标记通过其名称引用给定类型的标签。
标记 <list type="chapter"></list>:
该标记将已知标签的自动生成列表插入到文档中,例如目录或图列表。
除此之外,现有标题标记从 <h1> 到 <h6> 已经被扩展以便接受均为可选的附加 name 和 text 属性。这一点非常有用,因为这些标题标记对于 HTML 已经是现有的构造机制,而不是针对每个章节标题进行定义的额外 <lab> 标记,依赖于现有标记更有用。因此基本上不用
<lab name="Chapter1" type="chapter">The first chapter</lab>
而是使用
<h1 name="Chapter1">The first chapter</h1>
这样提供了一个极其重要的额外好处: 如果使用 HTML 标题标记构造目录的现有文档需要使用该工具进行处理,则不需要对文档做实际修改来获得章节标题的自动编号! 如果没有出现此类属性,XrefParser 对标题标记的 name 属性使用伪值。标题的编号惟一建立在文档结构基础上,而不是建立在标题标记的属性,因此,此类未修改的文档要使用正确的标题编号加以重写。当然,不可能添加指向此类标题的 <ref> 引用。然而此类附加信息根据需要可以被插入到现有文档中 —— 因此绝对不必为了获得该工具的某些好处而重写 HTML 文档。但是,要获得该工具的全部优点需要一定的重写.
在此给出几项注解:
使用任何标准的文本编辑器可以将这些标记插入到文档中。HTML 编辑器还支持 HTML 标记的插入(Netscape 编写器是提供该项功能的编辑器的一个示例 )。
在此采用的标记名称无可否认部分借用了 Leslie Lamport 的 LaTeX 宏软件包,该软件包建立在 Donald Knuth 极好的文本编辑器 TeX 之上。这可能是 sun 用于创建技术和科学文档的 最好 工具,特别是如果这些标记名称包含许多公式和等式的情况下尤为如此。
我们当然可以定义任何 HTML 标记的编号并将它们插入文档中 —— 这并不意味着没有这些标记浏览器就会变得重要了。幸运的是,浏览器不太关心这些标记而只是忽略它们。这样实际上很好,因为这意味着这些标记不在源文档中显示。在由该工具创建的目标文档中,已经将适当的文本(标题、表格或图编号 )添加到标记所指定的位置。
我们此处使用的 org.htmlparser.Parser 不 忽略这些标记当然是个好消息。事实上,它为每个标记提供了 org.htmlparser.lexer.nodes.TagNode 的一个实例,且 getRawTagName() 方法提供了标记名称作为可用于进一步处理的字符串。
XrefParser 不仅使用正确的编号解析所有的引用,而且还针对引用目标插入 HTML 锚点 ( <a name> ... </a> )和对引用提供超链接,因此导航得到充分的简化。还在由 <list> 标记所创建的列表内为每个项目创建超链接
对于所引入的 <ref> 和 <list> 标记,结束标记并不是在所有情况中都是必须的。但是,它是使用结束标记兼容 xHTML 是一个很好样式. 对于 <lab>标记,必须使这些结束标记顾及文本数据的自动收集。
技术实现
此处所选择的方法是定义被添加到 HTML 文档中的一些新 HTML 标记,并扩展带有附加属性的标准标题标记 <h1> 到 <h6> 。此类 HTML 文档使用 HTMLParser Sourceforge 项目的一部分 org.htmlparser.Parser 的实例加以处理。(参见 参考资料 部分 )。这个类提供一些功能来解析 HTML 文档并以树型结构的形式提供文档信息。该树型结构基本上由三个不同的节点元素组成:
org.htmlparser.lexer.nodes.TagNode: 一个含有关于解析器所遇到 HTML 标记的所有信息的节点。
org.htmlparser.lexer.nodes.TagNode: 一个含有纯 HTML 文本的节点。
org.htmlparser.lexer.nodes.TagNode: 一个含有关于解析器所遇到 HTML 注释的所有信息的节点。
该树型结构可以被递归处理,并根据所遇到节点的类型采取适当的操作。很明显,为达到 Xref 工具的目的,由于我们正寻找特定的标记来触发一定操作,TagNode 是最相关的节点,但是 StringNode 对于向文档添加附加信息也非常重要。
org.htmlparser.Parser 类 —— 或者更精确地讲,整个 HTMLParser 包 —— 的确是一个好软件,我为制作该软件的作者拍手喝彩! 它是一个健壮的解析器,可以不仅完美处理顺应 xHTML 的文档,而且还正确解析带有大量不那么符合样式指南的 HTML 文档,不过,对现今的浏览器来说它还是可接受的。可能此类 HTML 标记的最常见示例总是丢失像 </td> 或 </p> 这样的结束标记。不过,org.htmlparser.Parser 正确解析此类文档并生成文档树型结构。该结构可以被处理,使用类似下列示例代码:
//.... Parse a file whose name is stored in the String fileName
org.htmlparser.Parser parser = new org.htmlparser.Parser(fileName);
for (org.htmlparser.util.NodeIterator i = parser.elements();
i.hasMoreNodes(); ) {
org.htmlparser.Node node = i.nextNode();
nodes.add(node);
recurse(node);
}
其中,通过树的递归路径被实现,如下所示:
private void recurse(org.htmlparser.Node node) throws
org.htmlparser.util.ParserException {
//.... Do processing for the given node (depending on the node type)
...
//.... Recurse into children
org.htmlparser.util.NodeList nodeList = node.getChildren();
if (nodeList != null)
for (org.htmlparser.util.NodeIterator i = nodeList.elements(); i.hasMoreNodes(); )
recurse(i.nextNode());
}
org.htmlparser.Parser 类的一个非常方便的功能是不局限于已知的 HTML 标记集合,而是使用更多的模式匹配方式识别标记。这使得我们暗暗将新引入的标记插入文档,随后这些标记像标准的 HTML 标记那样作为 TagNode 实例加以报告。
一旦文档的树型结构可用,它就会被处理两次: 在第 1 次处理中,收集关于可引用对象的所有必要信息,第 2 次处理,解析在文档中所有对此类对象的引用,并执行对此类对象的自动编号。所有这些过程在 Xref 工具 至关紧要的 组件 ml.htmlkit.XrefParser 类中被实现。
因此,对这一过程做如下总结:
创建 XrefParser 的实例:
XrefParser parser = new XrefParser();
针对 inpfile 文件运行第 1 次处理:
parser.collect(inpfile);
这一过程创建深入实质的 org.htmlparser.Parser 以便创建文档树,然后正确使用上面所述的递归过程遍历该树并检查所有元素。
针对 inpfile 文件运行第 2 次处理:
parser.resolve(inpfile, outfile);
这一步再次递归遍历文档树(被存储在第 1 次处理中的 XrefParser 实例中 ) 以便解析所有引用,并在需要插入编号的地方插入编号。输出内容被写入给定的文件 outfile 中。
无可否认这是稍作简化的视图,但可以演示这一基本方法。
Xref 以容易使用的方式实现该过程。该应用程序接受以项目的形式描述要完成任务的 XML 文件,以及在文章的后边加以描述的详细内容。此时,指出以下几个方面很重要:
XrefParser 和 Xref 在项目内支持 多个 HTML 文档的处理。引用可以跨越多个文档,且所生成并被插入到文本中的链接将正确解析适当的文档。
XrefParser 接受一组参数以便控制生成输出的某些方面。对于这些默认选择的参数可以在描述交叉引用项目的 XML 文件内覆盖。
Xref 还可以被实例化且实际的交叉引用任务还可以通过调用
public void xrefFiles(java.util.List<String> inpfiles,
java.util.List<String> outfiles,
XrefParser parser) throws java.io.IOException,
org.htmlparser.util.ParserException
方法获得。这样允许从其他 Java 应用程序内使用该工具的这些功能。
还应当指出的是,该工具的初始版本依赖于在 Swing 包(是标准 JDK (javax.swing.text.html.HTMLEditorKit)的一部分)中所提供的 HTML 编辑器工具箱。该工具箱还包含一个可以容易应用于当前任务的 HTML 解析器。然而,不幸的是,解析器当前使用深入实质的 HTML 3.2 DTD (还会出现在即将发行的 J2SE 5.0 中 ),这意味着 HTML 4.0 (或更新版本 ) 功能可能引起这些问题。这些被观察的此类问题的示例之一是在 HTML 4.0 中被引入的实体(像 α )的不正确处理。由于一些最新引入的实体被认为对于 Xref 工具有用,最终,Swing 解析器会被没有这些局限性的 HTMLParser 包所替代。
名称空间概念
XrefParser 支持名称空间概念。这基本上意味着用于标签的名称被分配给不同的名称空间,且标签 —— 在名称空间 内 标签是惟一的 —— 当其属于不同的 名称空间时它们可以拥有自己的名称。名称空间还可以直接与实际正在被使用的编号有关,例如:对于表格或图。
名称空间通过使用在 J2SE 5.0 中的枚举、许多新的且很酷的开发简易性功能得以实现。
private enum NameSpace {
chapter,
figure,
table,
sequence1,
sequence2,
sequence3,
sequence4;
}
在第 1 次处理期间,XrefParser 收集标题和标签标记的信息。每一个此类标记属于一个名称空间: 鉴于对于标签标记的名称空间使用 type 属性加以选择,标题标记隐式属于 chapter 名称空间。
由于我们有准确控制允许值集合的 Java 类型,使用枚举向编码添加大量类型安全。这还可以在存储被收集信息的数据结构中加以使用。对于每个名称空间,该信息被存储在辅助类 TagInfo 的实例中。请注意J2SE 5.0 通用性的使用,以便控制可以在散列映射表中存储的数据类型:
private java.util.HashMap<NameSpace, TagInfo> tagInfo = new java.util.HashMap<NameSpace, TagInfo>();
在边注上经常有对所述代码的要求,以便检查在 HTML 文档(此处名称空间在 type 属性中加以指定 )中给定文本字符串是否是有效值,其次,我们意欲获得字符串所映射的枚举实例,以便在代码(例如在 switch 语句中 )中使用枚举。使用散列映射表可以轻松达到这一目的,例如:
java.util.HashMap<String, NameSpace> map = new
java.util.HashMap<String, NameSpace>();
该散列映射表可以使用映射到 NameSpace 枚举的字符串(名称空间 )加以填写:
for (NameSpace nameSpace: NameSpace.values())
map.put(nameSpace.toString(), nameSpace);
请注意 J2SE 5.0 增强的 for 循环的使用,还有新枚举构造的功能。当前可直截了当地获得上述目标:
通过散列映射表的 containsKey() 方法可以完成现有的检查。
通过散列映射表的 get() 方法可以完成获得枚举实例。
请注意,在 J2SE 5.0 中通用性功能确保当使用散列映射表时只有参数的正确类型可以被采用。除此之外,当从散列映射表检索对象时不必强制类型转换。
NameSpace nameSpace = map.get("chapter");
使用该方法确保在输入文档中非正式的打印错或仅被支持的数据可以容易被发现并加以报告。这种安全性可以以通用方式获得;我们可以通过简单修改 NameSpace 枚举轻松添加或移除名称空间,而且对于每个被支持的名称空间,我们不必维护在无休止的 if-then-else 构造中的字符串比较链。
正如前面已经提到的,名称空间概念与所采用的编码制有密切的关系。因此,如果在文档内有 12 个图和 7 张表格(或者一组文档 ),我们将为每个名称空间添加标签标记,但是对于图,使用 figure 名称空间,对于表格,使用 table 名称空间。那么,XrefParser 将为图插入从 1 到 12 的编号,为表格插入从 1 到 7 的编号。
请注意,这一概念不仅限于章节、图或表格 —— 如有必要,该概念可以应用到任何序列。HTML 当然会针对项目的自动编号提供 <ol> 标记,但是如果需要在通过使用 <ol> 标记(可能变得令人讨厌或在大型文档或一组文档之间不可能 )尚不能满足要求的文档中(或一组文档)动态创建并更新序列号,那么 XrefParser 以名称空间的形式提供了简单的解决方案。已经定义了 4 个通用性的多用途名称空间(从 sequence1 到 sequence4 ),并根据需要简单地通过向 NameSpace 枚举添加项目可以添加更多名称空间。因此,在整个文档内可以简单地使用
...
<lab type="sequence1" name="step1"></lab>
...
<lab type="sequence1" name="step2"></lab>
...
<lab type="sequence1" name="step3"></lab>
...
且 XrefParser 将这些标记重新格式化为
...
<lab type="sequence1" name="step1"></lab><a name="sequence1:step1">1</a>
...
<lab type="sequence1" name="step2"></lab><a name="sequence1:step2">2</a>
...
<lab type="sequence1" name="step3"></lab><a name="sequence1:step3">3</a>
...
如果有必要的话,在此所给出的名称还可以使用 <ref> 标记而被引用:
...
<ref type="sequence1" name="step2"></ref>
...
可以被
...
<ref type="sequence1" name="step2"></ref><a href="data.html#sequence1:step2">2</a>
...
代替,其中我们假设实际的 HTML 输出文档被命名为 data.html。除此之外,如果需要出现,<list> 标记可以被用于此类序列。
此类序列的一种可能应用是在文档内的文献引用列表。通常,由于此类引用通常在文档的结尾被收集,一个 HTML 有序列表可以完成确保以真实顺序添加编号的工作。在不进行手动维护这些引用编号的情况下,使用在列表中的真实编号引用文本内的此类引用 是不可能的。Xref 能轻松提供此项功能,如本示例中所示:
...
<lab type="sequence1" name="sun">
Sun's home page at http://www.sun.com
</lab><br>
<lab type="sequence1" name="sap">
SAP's home page at http://www.sap.com
</lab><br>
<lab type="sequence1" name="java">
The main Java page at http://java.sun.com
</lab><br>
...
引用如下所示:
... a lot of useful information on Java can be obtained from Sun's main Java
page [<ref type="sequence1" name="java"></ref>].
将被转换为
...
<lab type="sequence1" name="sun"><a name="sequence1:sun">1</a>
Sun's home page at http://www.sun.com
</lab><br>
<lab type="sequence1" name="sap"><a name="sequence1:sap">2</a>
SAP's home page at http://www.sap.com
</lab><br>
<lab type="sequence1" name="java"><a name="sequence1:java">3</a>
The main Java page at http://java.sun.com
</lab><br>
...
... a lot of useful information on Java can be obtained from Sun's main
Java page [<ref type="sequence1" name="java"></ref><a
href="doc.html#sequence1:java">3</a>].
章节是 一个 名称空间的所有组成部分,因此,由于在不同标题标记级别内存在分层结构,处理它们更复杂一些: 名称空间的主要编号应用于 <h1>,但是较低级别的标题标记级别依赖于较高级别标记: 如果我们遇到一个新的 <h(n)> 标记,对于 <h(n+1)> 的编号全部从 1 开始。对于该名称空间,要求在代码中有一些附加逻辑。
XrefParser 类
正如前面所述,XrefParser 这个类包含主要交叉引用逻辑。不过,在深入研究 XrefParser 详细内容前,采用了几个有必要加以描述的辅助类。
参数
XrefParser 支持几个控制其行为的参数。一个示例是控制用于 <h1> 级别标记的编号类型(这一样式可以是数字、罗马字母、希腊字符或 ASCII 字符或自定义样式 )的参数“H1Style”。有两种不同类型的这种参数: 种参数表示一个简单字符串值(ml.htmlkit.SimpleParameter ),另一种参数表示一组字符串类型值(ml.htmlkit.SetParameter )。两种实现类均继承了抽象基类 ml.htmlkit.Parameter:
public abstract class Parameter {
public enum Style {
Number,
RomanLower,
RomanUpper,
AlphaLower,
AlphaUpper,
GreekLower,
GreekUpper,
Custom,
None;
}
}
这个类有两个目的: 对于派生参数类而言,它是一个标记器;对于受支持的标题样式而言,它持有枚举。
参数通过使用从包含在 ml.htmlkit 包内的两个配置属性中获得的默认值加以预定义。XrefParser 提供了另外的方法以便更改这些默认值,并且在后面描述的项目 XML 文件还提供了 XML 标记以便控制每种参数。
SimpleParameter 的引用如下所示:
public class SimpleParameter extends Parameter {
private SimpleParameter.Name name = null;
private String value = null;
public enum Name {
H1Style,
H2Style,
H3Style,
H4Style,
H5Style,
H6Style,
H1H2Separator,
H2H3Separator,
H3H4Separator,
H4H5Separator,
H5H6Separator,
TextAfterChapter,
TextAfterFigure,
TextAfterTable;
}
...
}
再者,枚举被用于指定所支持的简单参数名称。当使用默认值、以控制对参数值访问的 XrefParser API 方法读取属性文件时,会使用它:
// A hash map mapping the names of simple parameters to enum instances
HashMap<String, SimpleParameter.Name> names
= new HashMap<String, SimpleParameter.Name>();
...
for (SimpleParameter.Name name: SimpleParameter.Name.values())
names.put(name.toString(), name);
...
// A hash map mapping enum instances to instances of SimpleParameter
HashMap<SimpleParameter.Name, SimpleParameter> params
= new HashMap<SimpleParameter.Name, SimpleParameter>();
...
Properties prop = new Properties();
properties.load(...); // Load the property file
// Setup the parameter data with the data read from the property file
for (Enumeration e = prop.propertyNames(); e.hasMoreElements(); ) {
String n = (String)e.nextElement();
if (names.containsKey(n)) {
params.put(names.get(n), new SimpleParameter(names.get(n), prop.getProperty(n)));
} else {
writeError("Unknown parameter: " + n);
}
}
请注意,检查在属性文件中的属性名称是否有效以及确保被存储在两个 HashMap 实例中的数据是所需要的正确类型是如此的简单和方便。编译器将标志不受支持的对象类型存储到映射表中的操作。
SimpleParameter 的其他 API 方法是:
public SimpleParameter(SimpleParameter.Name name, String value);
public SimpleParameter.Name getName();
public String getValue();
public void setValue(String value);
Name 枚举非常有用,确保在构造器中只有有效参数名称可以被指定。SetParameter 类稍微有些复杂:
public class SetParameter extends Parameter {
private java.util.ArrayList<String> values = null;
private SetParameter.Name name = null;
public enum Name {
RomanLowerSet,
RomanUpperSet,
GreekLowerSet,
GreekUpperSet,
AlphaLowerSet,
AlphaUpperSet,
CustomSet;
}
...
}
再者,枚举被用于有效参数名称,而 API 方法对于参数的这一类型稍微有些复杂:
public SetParameter(SetParameter.Name name, String valueString);
public SetParameter.Name getName();
public void addValue(String value);
public void removeValue(String value);
public java.util.ArrayList<String> getValues();
public void setValues(String valueString);
在此所选择的方法是,SetParameter 使用含有通过空格分开的有效值 valueString 得以初始化。标记名称列表(例如“h1 h2 h3 h4 h5 h6”)是一个示例。该字符串从内部被划分为存储于 ArrayList 中的单独字符串。这些值可以根据需要被添加到列表内或从该列表内被移除。
设置或修改设置参数还可以通过项目 XML 格式得以支持。下列表格列举了所有当前受支持的参数:
表 1: 参数名称及其含义
名称 | 类型 | 含义 | 有效值 |
H1Style | 简单 | 用于 <h1> 标题标记的样式 | Parameter.Style |
H2Style | 简单 | 用于 <h2> 标题标记的样式 | Parameter.Style |
H3Style | 简单 | 用于 <h3> 标题标记的样式 | Parameter.Style |
H4Style | 简单 | 用于 <h4> 标题标记的样式 | Parameter.Style |
H5Style | 简单 | 用于 <h5> 标题标记的样式 | Parameter.Style |
H6Style | 简单 | 用于 <h6> 标题标记的样式 | Parameter.Style |
H1H2Separator | 简单 | 用于 <h1> 和 <h2> 编号之间的分隔符。 | 任意字符串 |
H2H3Separator | 简单 | 用于 <h2> 和 <h3> 编号之间的分隔符。 | 任意字符串 |
H3H4Separator | 简单 | 用于 <h3> 和 <h4> 编号之间的分隔符。 | 任意字符串 |
H4H5Separator | 简单 | 用于 <h4> 和 <h5> 编号之间的分隔符。 | 任意字符串 |
H5H6Separator | 简单 | 用于 <h5> 和 <h6> 编号之间的分隔符。 | 任意字符串 |
TextAfterChapter | 简单 | 章节标题编号之后的文本。 | 任意字符串 |
TextAfterFigure | 简单 | 图编号之后的文本。 | 任意字符串 |
TextAfterTable | 简单 | 表格编号之后的文本。 | 任意字符串 |
RomanLowerSet | 集合 | 被用于编号样式 Parameter.Style.RomanLower 的各个字母 | i、ii、iii、iv ...... |
RomanLowerSet | 集合 | 被用于编号样式 Parameter.Style.RomanUpper 的各个字母 | I、II、III、IV ...... |
AlphaLowerSet | 集合 | 被用于编号样式 Parameter.Style.AlphaLower 的各个字母 | a、b、c ...... |
AlphaUpperSet | 集合 | 被用于编号样式 Parameter.Style.AlphaUpper 的各个字母 | A、B、C ...... |
GreekLowerSet | 集合 | 被用于编号样式 Parameter.Style.GreekLower 各个字母 | α、β、γ ...... |
GreekUpperSet | 集合 | 被用于编号样式 Parameter.Style.GreekUpper 各个字母 | Α、Β、 Γ ...... |
CustomSet | 集合 | 被用于编号样式 Parameter.Style.Custom 各个字母 | 任意一组字符串 |
将带有这些参数的这个类结构设计为可扩展的,以便假设有必要添加新的简单或集合参数时容易添加。
TagInfo 类
在文档处理的第一次处理期间,关于所遇到的各种标题和标签标记方面的信息被收集并加以存储,用于第 2 次处理。保存该信息的存储容器是提供下列 API 的TagInfo 类:
public boolean add(String name, String value, String prefix, String text);
public boolean containsName(String name);
public void reset();
public boolean hasNext();
public int size();
public void next();
public String getName();
public String getValue();
public String getValue(String name);
public String getPrefix(String name);
public String getText(String name);
public void setText(String name, String text);
public java.util.ArrayList<String> getNames();
目前对于每个名称空间存在保存所有标签信息的 TagInfo 实例。
add() 方法为每个标签获取 4 个参数:
名称: 这是标签可以被 <ref> 标记被引用的名称。对于标题标记(<h1>、<h2> ......),该名称既可以在 name属性中加以指定,或者如果没有找到 name 属性(可以不用于引用目的 ),也可以自动生成伪值。对于 <lab> 标签,该值从强制的 name 属性中获得。
值: 对于标签,这是被插入的值。对于标题标签,这是在章节题目前被插入的编号(像上面示例中的“3.1”),对于标签标记,这是被使用的当前编号而不是标签。除此之外,对于每项对名称所创建的引用,这是被插入的文本,且对于给定的名称空间,该文本还用于任何所创建的自动生成列表。
前缀: 对于自动生成的超链接,该文本被用作一个前缀,因此,通常该前缀将成为文件名称放在对锚点的引用之前,像在 <a href="chapter3.html#mySubTitle"> 中的“chapter3.html”。当文本跨越多个 HTML 文档时该前缀是必须的,且 Xref 完全支持在 <ref> 标记中使用这种前缀,还支持使用 <list> 标记所生成的任何列表中的前缀。
文本: 这是在自动生成列表中作为标签名称所使用的文本。
确定列表中的文本
Xref 工具的众多有用功能之一是其自动生成引用列表(像目录 )的功能。很显然,不仅有必要收集文档结构和各种标题和标签标记,而且还有必要收集需要在此类列表中显示的文本信息。
例如,所给定的文本片断类似于:
<h1>The first chapter</h1>
...
<lab type="figure" name="fig1>Average population density</lab>
...
我们高兴地看到包含下面一行的目录
1 第一章
以及包含下面一行的图列表:
1 平均人口密度
如何来完成这一结果呢? 这项任务并不繁琐,但是让我们看一看上述示例中的第一行以便解释这一问题如何被解决。对于 HTML 文本的这一行,org.htmlparser.Parser 创建了节点元素的序列:
TagNode: 开始 <h1> 标记
StringNode: 实际文本
TagNode: 结束 <h1> 标记
既然我们每次解析一个节点,文本收集进程只要当遇到相关开始标记 <h1> 时就会被激活。
Tag tag = ... // Derive from tag name of tag found by HTML parser
switch (tag) {
case h1: endTag = Tag.h1End; collectBuffer = new StringBuffer(); break;
case h2: endTag = Tag.h2End; collectBuffer = new StringBuffer(); break;
case h3: endTag = Tag.h3End; collectBuffer = new StringBuffer(); break;
case h4: endTag = Tag.h4End; collectBuffer = new StringBuffer(); break;
case h5: endTag = Tag.h5End; collectBuffer = new StringBuffer(); break;
case h6: endTag = Tag.h6End; collectBuffer = new StringBuffer(); break;
case label: endTag = Tag.labelEnd; collectBuffer = new StringBuffer();
}
此处的 Tag 是包含由 Xref 工具所观察到的所有开始和结束标记的枚举。这一内容将在 下一章 中进行详细描述。
在随后遇到的 StringNode 中所包含的文本被附加到收集缓冲区:
...
if ((node instanceof org.htmlparser.lexer.nodes.StringNode) && (endTag != null)) {
collectBuffer.append(' ');
collectBuffer.append(((org.htmlparser.lexer.nodes.StringNode)node).getText());
...
遇到匹配的结束标记:
if (endTag != null) { // In this case, we are in text collection mode
switch (tag) { // See if we have found the end tag we look for
case h1End:
case h2End:
case h3End:
case h4End:
case h5End:
case h6End:
case labelEnd:
String s = collectBuffer.toString().trim();
if (s.length() > 0 )
tagInfo.get(lastNameSpace).setText(lastName, s);
endTag = null;
}
}
}
如果遇到所需结束标记,下列几种情况会发生:
被收集的文本使用 setText() 方法被存储在适当的 TagInfo 元素中。该 TagInfo 实例的名称空间,以及我们为实际元素需要存储文本的名称(例如在上述情况中,开始 <h1> 标记 ) 之前已经分别存储在 lastNameSpace 和 lastName,以便允许直接访问正确的元素来存储文本值。
该过程还检查所收集的文本是否为空。由于解析器将在开始和结束标记之间报告不存在 <lab type="figure" name="fig1"></lab> 元素,对于像 StringNode 这样的构造这种情况会发生。在这种情况下,文本是实际名称空间的名称,使用默认的文本值
更多的文本收集通过设置 endTag 为 空 加以显示。
此处所采取的方法有一个额外的好处: 有时,在章节标题中所采用的可能是附加标记,例如
<h1>The <i>real</i> McCoy</h1>
通常此类附加标记在目录是不需要的,在此处,我们取消这一效果: 文本收集进程只占用 StringNode 节点,直到遇到所需的结束标记。对于文本收集进程,附加标记 <i> 和 </i> 被简单地忽略。
请注意,对于所有的 HTML 标题标记和标签标记 Xref 接受附加 text 属性,从而允许覆盖文本收集进程:
<h1 name="ChapterProxy" text="A Proxy server">
A lengthy heading describing a proxy server and what it's good for
</h1>
有时可能需要在开始和结束标记之间不使用实际文本,例如为了达到可读性和清晰的目的。使用 text 属性,该目的可以轻松达到,在上述示例中,在所生成的列表中采用了“代理服务器”。
实际 XrefParser 类
实际 XrefParser 类 —— 交叉索引功能的核心 —— 也充分使用枚举和通用性来简化编码任务,特别是参数的类型检查。
首先,在这个类中存在所定义的两个枚举。NameSpace 枚举已经做如下描述:
private enum NameSpace {
chapter,
figure,
table,
sequence1,
sequence2,
sequence3,
sequence4;
}
尽管 Tag 枚举已经在几处被使用,但还未充分加以解释:
private enum Tag {
h1("h1"),
h2("h2"),
h3("h3"),
h4("h4"),
h5("h5"),
h6("h6"),
list("list"),
label("lab"),
ref("ref"),
h1End("/h1"),
h2End("/h2"),
h3End("/h3"),
h4End("/h4"),
h5End("/h5"),
h6End("/h6"),
labelEnd("/lab");
private String name = null;
private Tag(String name) {
this.name = name;
}
public String getName() {
return name;
}
}
NameSpace 枚举正如其名称所暗示的,定义有效名称空间的集合。为了方便起见,请注意所添加的 4 个附加序列,且根据需要通过在源代码中扩展该枚举可以定义更多的名称空间。正如前一章节 “Namespace 概念”中的示例中所描述的,对于名称空间的默认行为,支持交叉文档自动编号。只有 章节 名称空间给予特殊处理以便说明标题标记的不同级别。
Tag 枚举定义 XrefParser 所感兴趣的标记。该枚举可以在开关语句中被方便使用来支持易于读取的编程样式。请注意,该枚举使用一些 J2SE 5.0 实现所提供的附加功能: 枚举可以有自己的方法(包括构造器 )和成员变量,我们在此处利用它是为了顾及在枚举元素的名称和 HTML 标记名称之间的抽象: 例如,枚举名称(例如 h1、 h2 或标签 )可以在 开关 语句中使用,枚举映射到的实际标记名称可以使用 getName() 方法检索。这一点非常方便,例如,为方便起见,称所引用的 <lab> 标记为 XrefParser.Tag.label ,还要考虑到标记名称要易于重新定义,需要: 仅在该枚举中更改一行就足以改变 Xref 使用的 HTML 标记名称,由于在其他地方只有枚举实例被使用。
XrefParser 类支持两种主要的操作: 标签和标题标记信息(第 1 次处理)的收集和引用信息的解析(第 2 次处理 )。因此,最感兴趣的方法是
public void collect(String inpfile) // Pass 1
public void resolve(String inpfile, String outfile) // Pass 2
在收集阶段,由 HTML 解析器返回的节点数据以散列映射表的形式被存储。对于每个输入文件,该散列映射表存储节点的 java.util.List 的一个实例:
private java.util.HashMap<String, java.util.List<org.htmlparser.Node>> nodeLists
= new java.util.HashMap<String, java.util.List<org.htmlparser.Node>>();
该数据之后在解析步骤中被重新使用。
收集进程的宏观视图在下列图中给出:
图 1: 收集进程 —— 宏观视图
collect() 方法具有下列结构:
public void collect(String fileName) throws java.io.IOException, org.htmlparser.util.ParserException {
...
//.... Setup the structure to hold the node information for this particular file
java.util.List<org.htmlparser.Node> nodes = new java.util.ArrayList<org.htmlparser.Node>();
nodeLists.put(fileName, nodes);
//.... Parse the file
org.htmlparser.Parser parser = new org.htmlparser.Parser(fileName);
org.htmlparser.Node node = null;
for (org.htmlparser.util.NodeIterator i = parser.elements(); i.hasMoreNodes(); ) {
node = i.nextNode();
nodes.add(node);
collectRecurse(node);
}
}
对于该文件,所有被解析器所报告的 org.htmlparser.Node 实例被存储在列表中。然后使用递归方法遍历每个节点的所有现有子节点:
private void collectRecurse(org.htmlparser.Node node) throws org.htmlparser.util.ParserException {
if ((node instanceof org.htmlparser.lexer.nodes.StringNode) && (endTag != null)) {
//.... Collect text data if we are in text collection mode
} else if (node instanceof org.htmlparser.lexer.nodes.TagNode) {
//.... Activate text collection if necessary
...
//.... Handle text collection if necessary (setText())
...
//.... Collect data for the tags. Store it in TagInfo instances.
switch (tag) {
case h1: ...
case h2: ...
case h3: ...
case h4: ...
case h5: ...
case h6: ...
case label: ...
}
}
}
//.... Recurse into children
...
}
对于所有输入文件,在所有数据被收集后执行解析步骤。解析进程的宏观视图在下列图中给出:
图 2: 解析进程 —— 宏观视图
resolve() 方法具有下列结构:
public void resolve(String inpfile, String outfile)
throws java.io.IOException, org.htmlparser.util.ParserException {
//.... Retrieve the previously stored document structure
java.util.List<org.htmlparser.Node> nodes = nodeLists.get(inpfile);
//.... Resolve
for (org.htmlparser.Node n : nodes)
resolveRecurse(n);
//.... Output
java.io.BufferedWriter writer = new java.io.BufferedWriter(new java.io.FileWriter(outfile));
for (org.htmlparser.Node n : nodes)
writer.write(n.toHtml());
writer.flush();
writer.close();
}
在此处,对于所有节点,我们还可以使用遍历过程来处理引用的解析:
private void resolveRecurse(org.htmlparser.Node node) throws org.htmlparser.util.ParserException {
...
switch (tag) {
//.... Chapter headings
case h1:
case h2:
case h3:
case h4:
case h5:
case h6:
... // Insert numbering and <a name ...> ... </a>
//.... Labels
case label:
... // Insert numbering and <a name ...> ... </a>
//.... References
case ref:
... // Insert reference number and <a href ...> ... </a>
//.... Lists
case list:
... // Insert a <table>
with <a
href ...> ... </a> elements
}
...
//.... Recurse into children
org.htmlparser.util.NodeList nodeList = node.getChildren();
if (nodeList != null)
for (org.htmlparser.util.NodeIterator i = nodeList.elements(); i.hasMoreNodes(); )
resolveRecurse(i.nextNode());
}
}
在此给出一个小技巧: 在几种情况下,附加的 HTML 标记,例如以自动生成 HTML 锚点的形式,被插入到文档结构中。通过创建并插入附加的 TagNode 实例并非对实际文档树进行修改,我们只是简单地将纯文本文件前置到 下一个 StringNode 元素,该元素随后触发此类附加标记创建的标记。
因此,例如,名称锚点将使用以下各行被组装到 StringBuffer:
sb.append("<a name="");
sb.append(NameSpace.chapter.toString());
sb.append(":");
sb.append(tagInfo.get(NameSpace.chapter).getName());
sb.append("">");
sb.append(tagInfo.get(NameSpace.chapter).getValue());
sb.append("</a>");
sb.append(simpleParameters.get(SimpleParameter.Name.TextAfterChapter).getValue());
然后使用 prev = sb.toString(); 将其存储到 prev 中。
接下来,org.htmlparser.lexer.nodes.StringNode 被找到,我们使用这个构造:
if (prev != null) {
org.htmlparser.lexer.nodes.StringNode stringNode = (org.htmlparser.lexer.nodes.StringNode)node;
stringNode.setText(prev + stringNode.getText());
prev = null;
}
对于名称锚点,通过注射附加标记,针对该字符串节点进行文本修改。
注意对于章节标题的实际编号存在不同的样式支持,这一点是非常重要的。将在 H*Style 参数基础上创建不同的输出样式,且章节标题类似
...
1.2.a
1.2.b
1.3
...
或
...
1.I
1.II
1.III
1.IV
...
是可能的。对于每个标题标记级别,这一样式可以被分别选择。有关受支持样式的更多信息,请参见 Parameter.Style 枚举文档。
我们已经投入相当大的精力使交叉引用进程无坚不摧,意味着错误输入数据被适当地检测并处理。如果此类问题被发现,宜采用有用的解决方法以便确保进程可以不管问题是否存在仍可继续,告警被写入默认为 STDERR (但可以被设置写入任意需要的写入程序 )的 java.io.Writer 实例。
Xref 和项目 XML 文件格式
最后,执行交叉引用所有的类全部到位,而使用这些类或许更容易些。对于每个文档或要处理的文档集合,使用 XrefParser 并非必须编写 Java 程序,而是以 Xref 应用程序的形式开发一种通用方法。该方法完全受 XML 文件(考虑了输入和输出文件的规范及各种要使用的参数值 )的控制。
下面是所给出的 XML 文件的一个示例:
<?xml version="1.0" encoding="ISO-8859-1"?>
<project>
<filepair inp="paper_01.html" out="paper_01_out.html"/>
<filepair inp="paper_02.html" out="paper_02_out.html"/>
<filepair inp="paper_03.html" out="paper_03_out.html"/>
<parameter name="H2H3Separator" value="-"/>
<parameter name="TextAfterChapter" value=". "/>
<set name="CustomSet" mode="set" value="10 11 12 13 14"/>
<set name="CustomSet" mode="add" value="15"/>
<set name="CustomSet" mode="remove" value="10"/>
</project>
在封闭的 <project> 标记内,另外三个标记得到支持:
filepair: 一对输入文件和其中写入输出的文件
parameter: 设置简单参数(覆盖默认值 )
set: 设置或修改集合参数。mode 属性接受“添加”、“移除”或“ 设置”值。
Xref 应用程序的最重要方法是 xrefFiles():
public void xrefFiles(java.util.List<String> inpfiles,
java.util.List<String> outfiles,
XrefParser parser) throws java.io.IOException,
org.htmlparser.util.ParserException
Xref 的 main()方法执行下列任务:
创建 Xref 实例:
创建 XrefParser 实例:
解析使用构建在 JDK 内的 XML 解析器作为参数所提供的项目 XML 文件。如有必要,设置输入和输出文件列表,并设置或修改 XrefParser 实例的参数。
调用 Xref 实例的 xrefFiles() 方法。
然后,实际的交叉引用由 xrefFiles() 执行,此时所选设计方法确保 Xref 也能够在应用程序内部被实例化。因此它们还可以调用 xrefFiles()。
以下给出了该方法的源代码:
public void xrefFiles(java.util.List<String> inpfiles,
java.util.List<String> outfiles,
XrefParser parser) throws java.io.IOException,
org.htmlparser.util.ParserException {
...
//.... Pass 1
for (int ifile = 0; ifile < inpfiles.size(); ifile++) {
parser.setLinkPrefix(outfiles.get(ifile));
parser.collect(inpfiles.get(ifile));
}
//.... Pass 2
for (int ifile = 0; ifile < inpfiles.size(); ifile++) {
parser.resolve(inpfiles.get(ifile), outfiles.get(ifile));
}
}
请注意,专用的 HTML 字符(像 XML 文件中的“a”)要求避免使用 & 号。在 XML 中是一个保留字符以指定实体引用,因此此类字符正确的语法如下所示:
<parameter name="TextAfterChapter" value="."/>
示例
本文的简化后的版本本身被用作演示 Xref 功能的示例。初始文件可以在 此处 查看,而被处理的文件从 此处 可得。
请注意在被修改的文件中所做的更该:
带有所有文本和超链接的目录、图列表和表格列表已被自动生成。
所有标题、表格和图已经自动编号。
演示程序引用(为文本“章节中的参考资料”加上前言 )已经被解析,且到正确章节标题的超链接已经被引入。
被用于该进程的项目 XML 文件如下所示:
<?xml version="1.0" encoding="ISO-8859-1"?>
<project>
<filepair inp="orig.html" out="mod.html"/>
<parameter name="H3Style" value="Number"/>
<parameter name="H2H3Separator" value=""/>
<parameter name="TextAfterChapter" value="."/>
</project>
在这种情况下,标题从 <h3> 级而不是 <h1> 级开始,仅仅因为 <h1> 格式化太大。为空字符串设置 H2H3Separator 以确保我们在前面的 h3 级编号不用句号结束,而是
.1. Introduction
我们现在得到
1. Introduction
除此之外,在标题后面的句号可通过向“.”设置 TextAfterchapter 加以创建。默认时,只使用空格。
在上述示例中,简介章节的标题在输入 HTML 文件中声明,如下所示:
<h3 name="h1">Introduction</h3>
Xref 在输出文件中将其转换为
<h3 name="h1"><a name="chapter:h1">1</a>.Introduction</h3>
请注意能在 <ref> 标记中被引用的自动生成 HTML 锚点。
最后要注意的一点: Xref 严重依赖于 org.htmlparser.Parser 解析器的解析功能。假设极大量 web 页面出自于解析器(在本开发项目的范围内明显没有被测试 ),就不可能存在该解析器难于处理一定数据的情况。因此,为本文描述的发行版所分配的 1.0 版本号是合理的,因为所有测试案例在开发期间被成功处理,但是与此同时,关于问题案例方面的 获取反馈 会有助于提高下一个版本。除此之外,Xref 不能修复带有中断结构的文档。当解析器在处理操作方面运行不佳时,例如丢失结束标记(像经常被忘记的 </td> 或 </p> ),如果标题标记的嵌套不正确(例如在 <h3> 内的 <h2> 等等 ),通过 Xref 被添加的编号将不是我们所预期的那样。此类问题既不能被解析器自动纠正,也不能被 Xref 自动纠正,宜应用 TITO (Trash-In -Trash-Out) 原理。
结束语
Xref 是一个主要支持结构化文本 web 内容的工具,希望它是一个有用的工具。该工具提供了从章节标题编号的简单自动生成到全自动交叉引用解决方案(包括对于可以跨越多个 HTML 文档的文本中任意对象的自动生成引用列表 )在内的几个复杂性级别。Xref 的实现还说明了在即将发行的 J2SE 5.0 (Tiger )中几个强有力的新型开发简易性功能,例如,通用性、枚举和增强的 for 循环。我已经发现这些功能对于本项目极其有用,而且我估计 —— 即使当在初始学习曲线中抽取这些功能时 —— 开发所投入的精力减少大约 30%,代码行数减少大约 40%。后者主要由于枚举的简易使用和枚举类型的安全性。
本文作者:未知