1# /=====================================================================\ #
2# |  LaTeXML::Common::Model                                             | #
3# | Stores representation of Document Type for use by Document          | #
4# |=====================================================================| #
5# | Part of LaTeXML:                                                    | #
6# |  Public domain software, produced as part of work done by the       | #
7# |  United States Government & not subject to copyright in the US.     | #
8# |---------------------------------------------------------------------| #
9# | Bruce Miller <bruce.miller@nist.gov>                        #_#     | #
10# | http://dlmf.nist.gov/LaTeXML/                              (o o)    | #
11# \=========================================================ooo==U==ooo=/ #
12package LaTeXML::Common::Model;
13use strict;
14use warnings;
15use LaTeXML::Global;
16use LaTeXML::Common::Object;
17use LaTeXML::Common::Error;
18use LaTeXML::Common::Font;
19use LaTeXML::Common::XML;
20use LaTeXML::Util::Pathname;
21use base qw(LaTeXML::Common::Object);
22
23#**********************************************************************
24my $LTX_NAMESPACE = "http://dlmf.nist.gov/LaTeXML";    # [CONSTANT]
25
26sub new {
27  my ($class, %options) = @_;
28  my $self = bless { xpath => LaTeXML::Common::XML::XPath->new(),
29    code_namespace_prefixes => {}, code_namespaces => {},
30    doctype_namespaces      => {},
31    namespace_errors        => 0,
32    %options }, $class;
33  $$self{xpath}->registerFunction('match-font', \&LaTeXML::Common::Font::match_font);
34  $self->registerNamespace('xml', $LaTeXML::Common::XML::XML_NS);
35  $self->registerDocumentNamespace('xml', $LaTeXML::Common::XML::XML_NS);
36  return $self; }
37
38sub setDocType {
39  my ($self, $roottag, $publicid, $systemid) = @_;
40  $$self{schemadata} = ['DTD', $roottag, $publicid, $systemid];
41  return; }
42
43sub setRelaxNGSchema {
44  my ($self, $schema) = @_;
45  $$self{schemadata} = ['RelaxNG', $schema];
46  return; }
47
48sub loadSchema {
49  my ($self) = @_;
50  return $$self{schema} if $$self{schema_loaded};
51  my $name;
52
53  if (!$$self{schemadata}) {
54    Warn('expected', '<model>', undef, "No Schema Model has been declared; assuming LaTeXML");
55    # article ??? or what ? undef gives problems!
56    $self->setRelaxNGSchema("LaTeXML");
57    $self->registerNamespace(ltx   => $LTX_NAMESPACE);
58    $self->registerNamespace(svg   => "http://www.w3.org/2000/svg");
59    $self->registerNamespace(xlink => "http://www.w3.org/1999/xlink");         # Needed for SVG
60    $self->registerNamespace(m     => "http://www.w3.org/1998/Math/MathML");
61    $self->registerNamespace(xhtml => "http://www.w3.org/1999/xhtml");
62    $$self{permissive} = 1; }    # Actually, they could have declared all sorts of Tags....
63
64  my ($type, @data) = @{ $$self{schemadata} };
65  if ($type eq 'DTD') {
66    my ($roottag, $publicid, $systemid) = @data;
67    require LaTeXML::Common::Model::DTD;
68    $name = $systemid;
69    $$self{schema} = LaTeXML::Common::Model::DTD->new($self, $roottag, $publicid, $systemid); }
70  elsif ($type eq 'RelaxNG') {
71    ($name) = @data;
72    require LaTeXML::Common::Model::RelaxNG;
73    $$self{schema} = LaTeXML::Common::Model::RelaxNG->new($self, $name); }
74
75  if (my $compiled = !$$self{no_compiled}
76    && pathname_find($name, paths => $STATE->lookupValue('SEARCHPATHS'),
77      types => ['model'], installation_subdir => "resources/$type")) {
78    $self->loadCompiledSchema($compiled); }
79  else {
80    $$self{schema}->loadSchema; }
81  $self->describeModel if $LaTeXML::Common::Model::DEBUG;
82  $$self{schema_loaded} = 1;
83  return $$self{schema}; }
84
85sub addSchemaDeclaration {
86  my ($self, $document, $tag) = @_;
87  $$self{schema}->addSchemaDeclaration($document, $tag);
88  return; }
89
90#=====================================================================
91# Make provision to precompile the schema.
92sub compileSchema {
93  my ($self) = @_;
94  $$self{no_compiled} = 1;
95  $self->loadSchema;
96  foreach my $prefix (sort keys %{ $$self{document_namespaces} }) {
97    print $prefix. '=' . $$self{document_namespaces}{$prefix} . "\n"; }
98  if (my $defs = $$self{schemaclass}) {
99    foreach my $classname (sort keys %$defs) {
100      print $classname. ':=(' . join(',', sort keys %{ $$self{schemaclass}{$classname} }) . ')' . "\n"; } }
101  foreach my $tag (sort keys %{ $$self{tagprop} }) {
102    next if $tag =~ /^!/;    # Ignore top-level negated (only make sense in content model)
103    print $tag
104      . '{' . join(',', sort keys %{ $$self{tagprop}{$tag}{attributes} }) . '}'
105      . '(' . join(',', sort keys %{ $$self{tagprop}{$tag}{model} }) . ')' . "\n"; }
106  return; }
107
108sub loadCompiledSchema {
109  my ($self, $file) = @_;
110  ProgressSpinup("Loading compiled schema $file");
111  my $MODEL;
112  open($MODEL, '<', $file) or Fatal('I/O', $file, undef, "Cannot open Compiled Model $file for reading", $!);
113  my $line;
114  while ($line = <$MODEL>) {
115    if ($line =~ /^([^\{]+)\{(.*?)\}\((.*?)\)$/) {
116      my ($tag, $attr, $children) = ($1, $2, $3);
117      $self->addTagAttribute($tag, split(/,/, $attr));
118      $self->addTagContent($tag, split(/,/, $children)); }
119
120    elsif ($line =~ /^([^:=]+):=(.*?)$/) {
121      my ($classname, $elements) = ($1, $2);
122      $self->setSchemaClass($classname, { map { ($_ => 1) } split(/,/, $elements) }); }
123    elsif ($line =~ /^([^=]+)=(.*?)$/) {
124      my ($prefix, $namespace) = ($1, $2);
125      $self->registerDocumentNamespace($prefix, $namespace); }
126    else {
127      Fatal('internal', $file, undef, "Compiled model '$file' is malformatted at \"$line\""); }
128  }
129  close($MODEL);
130  ProgressSpindown("Loading compiled schema $file");
131  return; }
132
133#**********************************************************************
134# Namespaces
135#**********************************************************************
136# There are TWO namespace mappings!!!
137# One for coding, one for the DocType.
138#
139# Coding: this namespace mapping associates prefixes to namespace URIs for
140#   use in the latexml code, constructors and such.
141#   This must be a one to one mapping and there are no default namespaces.
142# Document: this namespace mapping associates prefixes to namespace URIs
143#   as used in the generated document, and will be the
144#   set of prefixes used in the generated output.
145#   This mapping may also use a prefix of "#default" which is for
146#   the unprefixed form of elements (not used for attributes!)
147sub registerNamespace {
148  my ($self, $codeprefix, $namespace) = @_;
149  if ($namespace) {
150    $$self{code_namespace_prefixes}{$namespace} = $codeprefix;
151    $$self{code_namespaces}{$codeprefix}        = $namespace;
152    $$self{xpath}->registerNS($codeprefix, $namespace); }
153  else {
154    my $prev = $$self{code_namespaces}{$codeprefix};
155    delete $$self{code_namespace_prefixes}{$prev} if $prev;
156    delete $$self{code_namespaces}{$codeprefix}; }
157  return; }
158
159# In the following:
160#    $forattribute is 1 if the namespace is for an attribute (in which case, there must be a non-empty prefix)
161#    $probe, if non 0, just test for namespace, without creating an entry if missing.
162# Get the (code) prefix associated with $namespace,
163# creating a dummy prefix and signalling an error if none has been registered.
164sub getNamespacePrefix {
165  my ($self, $namespace, $forattribute, $probe) = @_;
166  if ($namespace) {
167    my $codeprefix = $$self{code_namespace_prefixes}{$namespace};
168    if ((!defined $codeprefix) && !$probe) {
169      my $docprefix = $$self{document_namespace_prefixes}{$namespace};
170      # if there's a doc prefix and it's NOT already used in code namespace mapping
171      if ($docprefix && !$$self{code_namespaces}{$docprefix}) {
172        $codeprefix = $docprefix; }
173      else {    # Else synthesize one
174        $codeprefix = "namespace" . (++$$self{namespace_errors}); }
175      $self->registerNamespace($codeprefix, $namespace);
176      Warn('malformed', $namespace, undef,
177        "No prefix has been registered for namespace '$namespace' (in code)",
178        "Using '$codeprefix' instead"); }
179    return $codeprefix; } }
180
181sub getNamespace {
182  my ($self, $codeprefix, $probe) = @_;
183  my $ns = $$self{code_namespaces}{$codeprefix};
184  if ((!defined $ns) && !$probe) {
185    $self->registerNamespace($codeprefix,
186      $ns = "http://example.com/namespace" . (++$$self{namespace_errors}));
187    Error('malformed', $codeprefix, undef,
188      "No namespace has been registered for prefix '$codeprefix' (in code)",
189      "Using '$ns' instead"); }
190  return $ns; }
191
192sub registerDocumentNamespace {
193  my ($self, $docprefix, $namespace) = @_;
194  $docprefix = '#default' unless defined $docprefix;
195  if ($namespace) {
196    # Since the default namespace url can still ALSO have a prefix associated,
197    # we prepend "DEFAULT#url" when using as a hash key in the prefixes table.
198    my $regnamespace = ($docprefix eq '#default' ? "DEFAULT#" . $namespace : $namespace);
199    $$self{document_namespace_prefixes}{$regnamespace} = $docprefix;
200    $$self{document_namespaces}{$docprefix}            = $namespace; }
201  else {
202    my $prev = $$self{document_namespaces}{$docprefix};
203    delete $$self{document_namespace_prefixes}{$prev} if $prev;
204    delete $$self{document_namespaces}{$docprefix}; }
205  return; }
206
207sub getDocumentNamespacePrefix {
208  my ($self, $namespace, $forattribute, $probe) = @_;
209  if ($namespace) {
210   # Get the prefix associated with the namespace url, noting that for elements, it might by "#default",
211   # but for attributes would never be.
212    my $docprefix = (!$forattribute && $$self{document_namespace_prefixes}{ "DEFAULT#" . $namespace })
213      || $$self{document_namespace_prefixes}{$namespace};
214    if ((!defined $docprefix) && !$probe) {
215      $self->registerDocumentNamespace($docprefix = "namespace" . (++$$self{namespace_errors}), $namespace);
216      Warn('malformed', $namespace, undef,
217        "No prefix has been registered for namespace '$namespace' (in document)",
218        "Using '$docprefix' instead"); }
219    return (($docprefix || '#default') eq '#default' ? '' : $docprefix); } }
220
221sub getDocumentNamespace {
222  my ($self, $docprefix, $probe) = @_;
223  $docprefix = '#default' unless defined $docprefix;
224  my $ns = $$self{document_namespaces}{$docprefix};
225  $ns =~ s/^DEFAULT#// if $ns;    # Remove the default hack, if present!
226  if (($docprefix ne '#default') && (!defined $ns) && !$probe) {
227    $self->registerDocumentNamespace($docprefix,
228      $ns = "http://example.com/namespace" . (++$$self{namespace_errors}));
229    Error('malformed', $docprefix, undef,
230      "No namespace has been registered for prefix '$docprefix' (in document)",
231      "Using '$ns' instead"); }
232  return $ns; }
233
234# Given a Qualified name, possibly prefixed with a namespace prefix,
235# as defined by the code namespace mapping,
236# return the NamespaceURI and localname.
237sub decodeQName {
238  my ($self, $codetag) = @_;
239  if ($codetag =~ /^([^:]+):(.+)$/) {
240    my ($prefix, $localname) = ($1, $2);
241    return (undef,                        $codetag) if $prefix eq 'xml';
242    return ($self->getNamespace($prefix), $localname); }
243  else {
244    return (undef, $codetag); } }
245
246sub encodeQName {
247  my ($self, $ns, $name) = @_;
248  my $codeprefix = $ns && $self->getNamespacePrefix($ns);
249  return ($codeprefix ? "$codeprefix:$name" : $name); }
250
251# Get the node's qualified name in standard form
252# Ie. using the registered (code) prefix for that namespace.
253# NOTE: Reconsider how _Capture_ & _WildCard_ should be integrated!?!
254sub getNodeQName {
255  my ($self, $node) = @_;
256  my $type = ($node ? $node->nodeType : -1);
257  if ($type == XML_TEXT_NODE) {
258    return '#PCDATA'; }
259  elsif ($type == XML_DOCUMENT_NODE) {
260    return '#Document'; }
261  elsif ($type == XML_COMMENT_NODE) {
262    return '#Comment'; }
263  elsif ($type == XML_PI_NODE) {
264    return '#ProcessingInstruction'; }
265  elsif ($type == XML_DTD_NODE) {
266    return '#DTD'; }
267  elsif ($type == XML_NAMESPACE_DECL) {
268    my $ns     = $node->declaredURI;
269    my $prefix = $ns && $self->getNamespacePrefix($ns, 0, 1);
270    return ($prefix ? 'xmlns:' . $prefix : 'xmlns'); }
271  # Need others?
272  elsif (($type != XML_ELEMENT_NODE) && ($type != XML_ATTRIBUTE_NODE)) {
273    Fatal('misdefined', '<caller>', undef,
274      "Should not ask for Qualified Name for node of type $type: " . Stringify($node));
275    return; }
276  else {
277    my $ns     = $node->namespaceURI;
278    my $prefix = $ns && $self->getNamespacePrefix($ns, 0, 1);
279    return ($prefix ? $prefix . ":" . $node->localname : $node->localname); } }
280
281# Same thing, but using the Document namespace prefixes
282sub getNodeDocumentQName {
283  my ($self, $node) = @_;
284  my $type = $node->nodeType;
285  if ($type == XML_TEXT_NODE) {
286    return '#PCDATA'; }
287  elsif ($type == XML_DOCUMENT_NODE) {
288    return '#Document'; }
289  elsif ($type == XML_COMMENT_NODE) {
290    return '#Comment'; }
291  elsif ($type == XML_PI_NODE) {
292    return '#ProcessingInstruction'; }
293  elsif ($type == XML_DTD_NODE) {
294    return '#DTD'; }
295  elsif ($type == XML_NAMESPACE_DECL) {
296    my $ns     = $node->declaredURI;
297    my $prefix = $ns && $self->getDocumentNamespacePrefix($ns, 0, 1);
298    return ($prefix ? 'xmlns:' . $prefix : 'xmlns'); }
299  # Need others?
300  elsif (($type != XML_ELEMENT_NODE) && ($type != XML_ATTRIBUTE_NODE)) {
301    Fatal('misdefined', '<caller>', undef,
302      "Should not ask for Qualified Name for node of type $type: " . Stringify($node));
303    return; }
304  else {
305    my $ns     = $node->namespaceURI;
306    my $prefix = $ns && $self->getDocumentNamespacePrefix($ns, 0, 1);
307    return ($prefix ? $prefix . ":" . $node->localname : $node->localname); } }
308
309# Given a Document QName, convert to "code" form
310# Used to convert a possibly prefixed name from the DTD
311# (using the DTD's prefixes)
312# into a prefixed name using the Code's prefixes
313# NOTE: Used only for DTD
314sub recodeDocumentQName {
315  my ($self,      $docQName) = @_;
316  my ($docprefix, $name)     = (undef, $docQName);
317  if ($docQName =~ /^(#PCDATA|#Comment|ANY|#ProcessingInstruction|#Document)$/) {
318    return $docQName; }
319  else {
320    ($docprefix, $name) = ($1, $2) if $docQName =~ /^([^:]+):(.+)/;
321    return $self->encodeQName($self->getDocumentNamespace($docprefix), $name); } }
322
323# Get an XPath context that knows about our namespace mappings.
324sub getXPath {
325  my ($self) = @_;
326  return $$self{xpath}; }
327
328#**********************************************************************
329# Accessors
330#**********************************************************************
331
332sub getTags {
333  my ($self) = @_;
334  return (sort keys %{ $$self{tagprop} }); }
335
336sub getTagContents {
337  my ($self, $tag) = @_;
338  my $h = $$self{tagprop}{$tag}{model};
339  return ($h ? sort keys %$h : ()); }
340
341sub addTagContent {
342  my ($self, $tag, @elements) = @_;
343  $$self{tagprop}{$tag}{model} = {} unless $$self{tagprop}{$tag}{model};
344  map { $$self{tagprop}{$tag}{model}{$_} = 1 } @elements;
345  return; }
346
347sub getTagAttributes {
348  my ($self, $tag) = @_;
349  my $h = $$self{tagprop}{$tag}{attributes};
350  return $h ? sort keys %$h : (); }
351
352sub addTagAttribute {
353  my ($self, $tag, @attributes) = @_;
354  $$self{tagprop}{$tag}{attributes} = {} unless $$self{tagprop}{$tag}{attributes};
355  map { $$self{tagprop}{$tag}{attributes}{$_} = 1 } @attributes;
356  return; }
357
358sub setSchemaClass {
359  my ($self, $classname, $content) = @_;
360  $$self{schemaclass}{$classname} = $content;
361  return; }
362
363#**********************************************************************
364# Document Structure Queries
365#**********************************************************************
366# NOTE: These are public, but perhaps should be passed
367# to submodel, in case it can evolve to more precision?
368# However, it would need more context to do that.
369
370# Can an element with (qualified name) $tag contain a $childtag element?
371sub canContain {
372  my ($self, $tag, $childtag) = @_;
373  $self->loadSchema unless $$self{schema_loaded};
374  # Handle obvious cases explicitly.
375  return 0 if !$tag || ($tag eq '#PCDATA') || ($tag eq '#Comment');
376  return 1 if $tag =~ /(.*?:)?_Capture_$/;                          # with or without namespace prefix
377  return 1 if $tag eq '_WildCard_';
378  return 1 if $childtag =~ /(.*?:)?_Capture_$/;
379  return 1 if $childtag eq '_WildCard_';
380  return 1 if $childtag eq '#Comment';
381  return 1 if $childtag eq '#ProcessingInstruction';
382  return 1 if $childtag eq '#DTD';
383  #  return 1 if $$self{permissive}; # No DTD? Punt!
384  return 1 if $$self{permissive} && ($tag eq '#Document') && ($childtag ne '#PCDATA'); # No DTD? Punt!
385      # Else query tag properties.
386  my $model = $$self{tagprop}{$tag}{model};
387  if (!$model && ($tag =~ /^(\w*):/)) {
388    my $xtag = $1 . ':*';
389    $model = $$self{tagprop}{$xtag}{model}; }
390  my ($chns, $chname) = ($childtag =~ /^([^:]*):(.*)$/ ? ($1, $2) : (undef, $childtag));
391  if ($chns) {
392    return ($$model{$childtag} ? 1
393      : ($$model{"!$childtag"} ? 0
394        : ($$model{"$chns:*"} ? 1
395          : ($$model{"!$chns:*"} ? 0
396            : ($$model{ANY} ? 1
397              : 0))))); }
398  else {
399    return ($$model{$childtag} ? 1
400      : ($$model{"!$childtag"} ? 0
401        : ($$model{ANY} ? 1
402          : 0))); } }
403
404# NOTE: Currently the Document class already allows ANY namespaced attributes!
405# (which is very unmodular, although it does have to arrange for namespace declarations)
406sub canHaveAttribute {
407  my ($self, $tag, $attrib) = @_;
408  $self->loadSchema unless $$self{schema_loaded};
409  return 0 if $tag eq '#PCDATA';
410  return 0 if $tag eq '#Comment';
411  return 0 if $tag eq '#Document';
412  return 0 if $tag eq '#ProcessingInstruction';
413  return 0 if $tag eq '#DTD';
414  return 1 if $tag =~ /(.*?:)?_Capture_$/;
415  return 1 if $$self{permissive};
416  my $attr = $$self{tagprop}{$tag}{attributes};
417
418  if (!$attr && ($tag =~ /^(\w*):/)) {
419    my $xtag = $1 . ':*';
420    $attr = $$self{tagprop}{$xtag}{attributes}; }
421  return $$attr{ANY} || $$attr{$attrib}; }
422
423sub isInSchemaClass {
424  my ($self, $classname, $tag) = @_;
425  $tag = $self->getNodeQName($tag) if ref $tag;    # In case tag is a node.
426  my $class = $$self{schemaclass}{$classname};
427  return $class && $$class{$tag}; }
428
429#**********************************************************************
430sub describeModel {
431  my ($self) = @_;
432  Debug("Doctype");
433  foreach my $tag (sort keys %{ $$self{tagprop} }) {
434    if (my $model = $$self{tagprop}{$tag}{model}) {
435      if (keys %$model) {
436        Debug("$tag can contain " . join(', ', sort keys %{ $$self{tagprop}{$tag}{model} })); } }
437    else {
438      Debug("$tag is empty"); }
439  }
440  return; }
441
442#**********************************************************************
4431;
444
445__END__
446
447=pod
448
449=head1 NAME
450
451C<LaTeXML::Common::Model> - represents the Document Model
452
453=head1 DESCRIPTION
454
455C<LaTeXML::Common::Model> encapsulates information about the document model to be used
456in converting a digested document into XML by the L<LaTeXML::Core::Document>.
457This information is based on the document schema (eg, DTD, RelaxNG),
458but is also modified by package modules; thus the model may not be
459complete until digestion is completed.
460
461The kinds of information that is relevant is not only the content model
462(what each element can contain contain), but also SGML-like information
463such as whether an element can be implicitly opened or closed, if needed
464to insert a new element into the document.
465
466Currently, only an approximation to the schema is understood and used.
467For example, we only record that certain elements can appear within another;
468we don't preserve any information about required order or number of instances.
469
470It extends L<LaTeXML::Common::Object>.
471
472=head2 Model Creation
473
474=over 4
475
476=item C<< $model = LaTeXML::Common::Model->new(%options); >>
477
478Creates a new model.  The only useful option is
479C<< permissive=>1 >> which ignores any DTD and allows the
480document to be built without following any particular content model.
481
482=back
483
484=head2 Document Type
485
486=over 4
487
488=item C<< $model->setDocType($rootname,$publicid,$systemid,%namespaces); >>
489
490Declares the expected rootelement, the public and system ID's of the document type
491to be used in the final document.  The hash C<%namespaces> specifies
492the namespace prefixes that are expected to be found in the DTD, along with
493the associated namespace URI.  These prefixes may be different from
494the prefixes used in implementation code (eg. in ltxml files; see RegisterNamespace).
495The generated document will use the namespaces and prefixes defined here.
496
497=back
498
499=head2 Namespaces
500
501Note that there are I<two> namespace mappings between namespace URIs and prefixes
502that are relevant to L<LaTeXML>.
503The `code' mapping is the one used in code implementing packages, and in
504particular, constructors defined within those packages.  The prefix C<ltx>
505is used consistently to refer to L<LaTeXML>'s own namespace
506(C<http://dlmf.nist.gov/LaTeXML)>.
507
508The other mapping, the `document' mapping, is used in the created document;
509this may be different from the `code' mapping in order to accommodate
510DTDs, for example, or for use by other applications that expect
511a rigid namespace mapping.
512
513=over 4
514
515=item C<< $model->registerNamespace($prefix,$namespace_url); >>
516
517Register C<$prefix> to stand for the namespace C<$namespace_url>.
518This prefix can then be used to create nodes in constructors and Document methods.
519It will also be recognized in XPath expressions.
520
521=item C<< $model->getNamespacePrefix($namespace,$forattribute,$probe); >>
522
523Return the prefix to use for the given C<$namespace>.
524If C<$forattribute> is nonzero, then it looks up the prefix as appropriate for attributes.
525If C<$probe> is nonzero, it only probes for the prefix, without creating a missing entry.
526
527=item C<< $model->getNamespace($prefix,$probe); >>
528
529Return the namespace url for the given C<$prefix>.
530
531=back
532
533=head2 Model queries
534
535=over 2
536
537=item C<< $boole = $model->canContain($tag,$childtag); >>
538
539Returns whether an element with qualified name C<$tag> can contain an element
540with qualified name C<$childtag>.
541The tag names #PCDATA, #Document, #Comment and #ProcessingInstruction
542are specially recognized.
543
544=item C<< $boole = $model->canHaveAttribute($tag,$attribute); >>
545
546Returns whether an element with qualified name C<$tag> is allowed to have an attribute
547with the given name.
548
549=back
550
551=head1 SEE ALSO
552
553L<LaTeXML::Common::Model::DTD>,
554L<LaTeXML::Common::Model::RelaxNG>.
555
556=head1 AUTHOR
557
558Bruce Miller <bruce.miller@nist.gov>
559
560=head1 COPYRIGHT
561
562Public domain software, produced as part of work done by the
563United States Government & not subject to copyright in the US.
564
565=cut
566