Updated release tag for trunk changes 4328 to 4330.
[docutils.git] / src / Transforms.pm
blobd1d88bb3f625a4a74b2c5033b9473a4242cfb44a
1 # $Id$
2 # Copyright (C) 2002-2005 Freescale Semiconductor, Inc.
3 # Distributed under terms of the GNU General Public License (GPL).
5 @Transforms::TRANSFORMS = qw(docutils.transforms.references.MarkReferenced
6 docutils.transforms.references.IndTargets
7 docutils.transforms.frontmatter.DocTitle
8 docutils.transforms.frontmatter.SectionSubTitle
9 docutils.transforms.frontmatter.DocInfo
10 docutils.transforms.references.CitationReferences
11 docutils.transforms.misc.Pending
12 docutils.transforms.universal.EmptyTopics
13 docutils.transforms.references.AutoFootnotes
14 docutils.transforms.references.FootnoteReferences
15 docutils.transforms.references.References
16 docutils.transforms.references.Unreferenced
17 docutils.transforms.universal.Transitions
18 docutils.transforms.universal.ScoopMessages
19 docutils.transforms.universal.Messages
20 docutils.transforms.universal.Decorations
23 package docutils::transforms::components;
25 =pod
26 =begin reST
27 =begin Usage
28 Defines for reStructuredText transforms
29 ---------------------------------------
30 -D generator=<0|1> Include a "Generated by" credit at the end of
31 the document (default is 1).
32 -D date=<0|1> Include the date at the end of the document
33 (default is 0).
34 -D time=<0|1> Include the date and time at the end of the
35 document (default is 1, overrides date if 1).
36 -D source_link=<0|1> Include a "View document source" link (default
37 is 1).
38 -D source_url=<URL> Use the supplied <URL> verbatim for a "View
39 document source" link; implies -D source_link=1.
40 -D keep_title_section Keeps the section intact from which the document
41 title is taken.
42 -D section_subtitles Activate the promotion of lone subsection titles to
43 section subtitles.
44 =end Usage
45 =end reST
46 =cut
48 # Global variables:
49 # ``@Transforms::TRANSFORMS``
50 # Array of transform names in the order they will be applied.
52 use strict;
54 # Processes a docutils.transforms.components.Filter transform.
55 # Arguments: pending DOM, top-level DOM, details hash reference
56 sub Filter{
57 my ($dom, $topdom, $details) = @_;
59 if ($main::opt_w eq eval($details->{format}) ||
60 $main::opt_w eq 'dom') {
61 my $nodes = $details->{nodes};
62 return new DOM($nodes->{tag}, %{$nodes->{attr}});
64 return;
67 package docutils::transforms::frontmatter;
69 use vars qw(%BIB_ELEMENTS);
71 BEGIN {
72 my @bib_elements = qw(author authors organization address contact version
73 revision status date copyright dedication abstract);
74 @BIB_ELEMENTS{@bib_elements} = (1) x @bib_elements;
77 # Processes a docutils.transforms.frontmatter.DocInfo transform.
78 # Processes field lists at the beginning of the DOM that are one of
79 # the docinfo types into a docinfo section.
80 # Arguments: top-level DOM
81 sub DocInfo {
82 my ($dom) = @_;
84 # Create a docinfo if needed
85 my @field_lists = grep($_->{tag} eq 'field_list', $dom->contents());
86 my %element_seen;
87 if (@field_lists) {
88 my $fl = $field_lists[0];
89 my @content = $fl->contents();
90 # Modify the field list in situ
91 $fl->{tag} = 'docinfo';
92 $fl->replace();
93 my $docinfo = $fl;
94 my $field;
95 my @postdocinfo; # Things to be added to content list after docinfo
96 foreach $field (@content) {
97 my $fn = $field->{content}[0];
98 my $fb = $field->{content}[1];
99 my $name = $fn->{content}[0]{text};
100 my $origname = $name;
101 $name =~ tr/A-Z/a-z/;
102 my $tname = $name;
103 substr($tname,0,1) =~ tr/ad/AD/;
104 if ($BIB_ELEMENTS{$name}) {
105 $element_seen{$name}++;
106 if ($element_seen{$name} > 1 && $name =~ /abstract/) {
107 $fb->append
108 (RST::system_message(2, $field->{source},
109 $field->{lineno},
110 qq(There can only be one "$tname" field.)));
111 $docinfo->append($field);
113 elsif ($name =~ /^(dedication|abstract)$/) {
114 my $topic = new DOM('topic', classes=>[ $name ]);
115 my $title = new DOM('title');
116 $topic->append($title);
117 $title->append(newPCDATA DOM($tname));
118 $topic->append($fb->contents());
119 push(@postdocinfo, $topic);
121 elsif ($fb->num_contents() < 1) {
122 $fb->append
123 (RST::system_message(2, $field->{source},
124 $field->{lineno},
125 qq(Cannot extract empty bibliographic field "$origname".)));
126 $docinfo->append($field);
128 elsif ($name eq 'authors') {
129 my $bib = new DOM($name);
130 my @contents = $fb->{content}[0]->contents();
131 # There are three cases: bullet_lists,
132 # multiple paragraphs, and string.
133 if (($fb->num_contents() == 1 &&
134 ($fb->{content}[0]{tag} !~
135 /paragraph|bullet_list/ ||
136 $fb->{content}[0]{tag} eq 'bullet_list' &&
137 grep($_->num_contents() != 1 ||
138 $_->{content}[0]{tag} ne 'paragraph',
139 $fb->{content}[0]->contents()))
140 ) ||
141 ($fb->num_contents() > 1 &&
142 grep($_->{tag} ne 'paragraph', $fb->contents()))
144 $fb->append
145 (RST::system_message(2, $field->{source},
146 $field->{lineno},
147 qq(Bibliographic field "Authors" incompatible with extraction: it must contain either a single paragraph (with authors separated by one of ";,"), multiple paragraphs (one per author), or a bullet list with one paragraph (one author) per item.)));
148 $docinfo->append($field);
150 elsif ($fb->num_contents() > 1) {
151 # Multiple paragraphs
152 foreach ($fb->contents()) {
153 my $author = new DOM('author');
154 $bib->append($author);
155 $author->append($_->contents());
158 elsif ($fb->{content}[0]{tag} eq 'bullet_list') {
159 my $bl = $fb->{content}[0];
160 foreach ($bl->contents()) {
161 my $author = new DOM('author');
162 $bib->append($author);
163 $author->append($_->{content}[0]->contents());
166 else {
167 my $text;
168 $fb->Recurse(sub {
169 my ($dom) = @_;
170 $text .= $dom->{text}
171 if $dom->{tag} eq '#PCDATA';
173 my @authors = $text =~ /;/ ?
174 split(/\s*;\s*/, $text) :
175 split(/\s*,\s*/, $text);
176 foreach (@authors) {
177 my $author = new DOM('author');
178 $bib->append($author);
179 $author->append(newPCDATA DOM($_));
182 if ($bib->num_contents() == 1) {
183 $docinfo->append($bib->{content}[0]);
185 elsif ($bib->num_contents() > 1) {
186 $docinfo->append($bib);
189 elsif ($fb->num_contents() > 1) {
190 $fb->append
191 (RST::system_message(2, $field->{source},
192 $field->{lineno},
193 qq(Cannot extract compound bibliographic field "$origname".)));
194 $docinfo->append($field);
196 elsif ($fb->{content}[0]{tag} ne 'paragraph') {
197 $fb->append
198 (RST::system_message(2, $field->{source},
199 $field->{lineno},
200 qq(Cannot extract bibliographic field "$origname" containing anything other than a single paragraph.)));
201 $docinfo->append($field);
203 else {
204 my $bib = new DOM($name);
205 %{$bib->{attr}} = (%RST::XML_SPACE)
206 if $name =~ /^address$/i;
207 $docinfo->append($bib);
208 my @contents = $fb->{content}[0]->contents();
209 my $pcdata = $contents[0];
210 $pcdata->{text} =~ s/\$\w+:\s*(.+?)(?:,v)?\s\$/$1/g
211 if defined $pcdata->{text};
212 $bib->append(@contents);
215 else {
216 $docinfo->append($field);
220 # Anything before the docinfo that's not a title, subtitle, or
221 # decoration has to move after it.
222 my $i;
223 my $docinfo_seen = 0;
224 my @new_content;
225 for ($i=0; $i < $dom->num_contents(); $i++) {
226 my $c = $dom->{content}[$i];
227 if ($docinfo_seen || $c->{tag} =~ /^((sub)?title|decoration)$/) {
228 push @new_content, $c;
230 elsif ($c->{tag} eq 'docinfo') {
231 $docinfo_seen = 1;
232 push @new_content, $c, @postdocinfo;
234 else {
235 push @postdocinfo, $c;
238 $dom->replace(@new_content);
239 # $dom->{content} = \@new_content;
243 # Processes a docutils.transforms.frontmatter.DocTitle transform.
244 # Creates a document title if the top-level DOM has only one top-level
245 # section. Creates a subtitle if a unique top-level section has a
246 # unique second-level section.
247 # Arguments: top-level DOM
248 sub DocTitle {
249 my ($dom) = @_;
251 create_title($dom);
252 $dom->{attr}{title} = $dom->{'.details'}{title}
253 if defined $dom->{'.details'}{title};
254 return;
257 # Processes a docutils.transforms.frontmatter.SectionSubTitle transform.
258 # Creates a subtitle if a section DOM has only one top-level
259 # subsection.
260 # Arguments: top-level DOM
261 sub SectionSubTitle {
262 my ($topdom) = @_;
264 # Link references to their definitions if they exist
265 $topdom->Reshape
266 (sub {
267 my($dom) = @_;
268 if ($dom->{tag} eq 'section') {
269 create_title($dom, 1);
271 return $dom;
273 , 'pre') if $main::opt_D{section_subtitles};
274 return;
277 # Used to turn a lone section into a title/subtitle of the given DOM
278 # Arguments: section DOM object
279 # Returns: None
280 # Side-effects: May reorganize the contents to promote a lone section
281 sub create_title {
282 my ($dom) = @_;
283 # If the document has one section, coalesce it with the DOM
284 my @sections = grep($_->{tag} eq 'section', $dom->contents());
286 if (@sections == 1 && ($main::opt_D{keep_title_section} ||
287 !grep($_->{tag} !~
288 /^(section|comment|system_message|target|substitution_definition|title|decoration)$/,
289 $dom->contents()))) {
290 my $sec = $sections[0];
291 my @non_sections = grep($_->{tag} !~ /^(?:section|title)$/,
292 $dom->contents());
293 my ($prev_title) = grep $_->{tag} eq 'title', $dom->contents();
294 # Get the title text
295 my $ttext = '';
296 $sec->{content}[0]->Recurse(sub {
297 my ($dom) = @_;
298 $ttext .= $dom->{text} if $dom->{tag} eq '#PCDATA';
300 chomp $ttext;
301 my $dom_ids = $dom->{attr}{ids};
302 if (! $prev_title) {
303 $dom->{attr}{title} =
304 $ttext; #RST::NormalizeName($ttext, 'keepcase');
305 @{$dom->{attr}}{keys %{$sec->{attr}}} = values %{$sec->{attr}};
307 if ($main::opt_D{keep_title_section} && ! defined $prev_title) {
308 # Don't duplicate ids from the section if we keep the section
309 if ($dom_ids) {
310 $dom->{attr}{ids} = $dom_ids;
312 else {
313 delete $dom->{attr}{ids};
315 my $title = $sec->{content}[0];
316 $dom->prepend($title);
318 else {
319 $dom->{content} = $sec->{content};
320 $dom->splice(1, 0, @non_sections);
322 if (defined $prev_title) {
323 my $subtitle = $dom->{content}[0];
324 $subtitle->{tag} = 'subtitle';
325 $subtitle->{attr}{ids} = [ RST::NormalizeId($ttext) ];
326 $subtitle->{attr}{names} = [ RST::NormalizeName($ttext) ];
327 $dom->prepend($prev_title);
329 else {
330 # Check for a subtitle
331 my @sections = grep($_->{tag} eq 'section', $dom->contents());
332 if (@sections == 1) {
333 my $sec = $sections[0];
334 my $title = $sec->splice(0, 1);
335 my @non_sections = grep($_->{tag} !~ /^(section|title)$/,
336 $dom->contents());
337 $sec->prepend(@non_sections);
338 $title->{tag} = 'subtitle';
339 $title->{attr} = $sec->{attr};
340 $dom->replace(grep($_->{tag} eq 'title',
341 $dom->contents()));
342 $dom->append($title, $sec->contents());
349 package docutils::transforms::misc;
351 # This package contains routines for transforms of DOM trees
353 # Processes a docutils.transforms.misc.Pending transform.
354 # Traverses the DOM tree looking for Pending nodes and applies
355 # whatever internal transform was specified for them.
356 # Arguments: top-level DOM
357 sub Pending {
358 my ($topdom) = @_;
360 # Handle pending transformations
361 $topdom->Reshape
362 (sub {
363 my($dom) = @_;
364 if ($dom->{tag} eq 'pending') {
365 my $transform = $dom->{internal}{'.transform'};
366 (my $t = $transform) =~ s/\./::/g;
367 return RST::system_message(4, $dom->{source},
368 $dom->{lineno},
369 qq(No transform code found for "$transform".))
370 unless defined &$t;
371 my $details = $dom->{internal}{'.details'};
372 no strict 'refs';
373 print STDERR "Debug: Transform $transform\n" if $main::opt_d;
374 return &$t($dom, $topdom, $details);
376 return $dom;
380 package docutils::transforms::parts;
382 # This package contains routines for transforms of DOM trees
384 # Processes a docutils.transforms.parts.Class transform.
385 # Arguments: pending DOM, top-level DOM, details hash reference
386 sub Class {
387 my ($dom, $topdom, $details) = @_;
389 my $next = $dom->next('comment|substitution_definition|target|system_message|pending');
390 my $tag = $next->{tag} if defined $next;
391 if (defined $tag && $tag =~ /^(?:paragraph|.*_list|section|.*_block|block_quote|table|figure|raw)$/) {
392 # It's a classable tag
393 push @{$next->{attr}{classes}}, split(/\s+/, $details->{class});
394 return;
396 return RST::system_message(2, $dom->{source}, $dom->{lineno},
397 qq(Error in "class" directive: there is no following block element for which a class can be specified.),
398 $dom->{lit})
401 # Processes a docutils.transforms.parts.Contents transform.
402 # Arguments: pending DOM, top-level DOM, details hash reference
403 sub Contents {
404 my ($dom, $topdom, $details) = @_;
405 my @errs;
407 my $parent = $dom->parent();
408 my $backlinks =
409 defined $details->{backlinks} ? $details->{backlinks} : '';
410 # First we compile the table of contents
411 my $contid = $parent->{attr}{ids}[0] if defined $parent->{attr}{ids};
413 my $bl = new DOM('bullet_list');
414 my $depth = 0; # Used in closure of sub
415 my @list = ($bl); # Used in closure of sub
416 my $start = defined $details->{local} ? $dom->{section} : $topdom;
417 $start->Recurse
418 (sub {
419 my($dom, $when) = @_;
421 if ($dom->{tag} eq 'section' && $dom ne $start) {
422 $depth-- if $when eq 'post';
423 if (! defined $details->{depth} ||
424 $depth < $details->{depth}) {
425 my $bl = $list[-1];
426 my $li;
427 if ($when eq 'pre') {
428 my $id = RST::Id();
429 $li = new DOM('list_item'); #, ids=>$id);
430 $bl->append($li);
431 if ($backlinks !~ /none/i &&
432 $dom->{content}[0]{content}[0]{tag} ne 'reference') {
433 $dom->{content}[0]{attr}{refid} =
434 ($backlinks =~ /top/i) ? $contid : $id;
436 my $para = new DOM('paragraph');
437 $li->append($para);
438 my $ref = new DOM('reference', ids=> [ $id ],
439 refid=>$dom->{attr}{ids}[0]);
440 $para->append($ref);
441 my @contents; # Used in the closure of the sub
442 $dom->{content}[0]->Recurse
443 (sub {
444 my ($dom, $when) = @_;
445 my $tag = $dom->{tag};
446 if ($tag =~ /^(?:title|(footnote|citation)_reference|interpreted|problematic|reference|target)$/) {
447 # Ignore
449 elsif ($tag =~ /image/) {
450 push(@contents,
451 newPCDATA DOM($dom->{attr}{alt}))
452 if defined $dom->{attr}{alt} &&
453 $when eq 'pre';
455 else {
456 if ($when eq 'pre') {
457 # Don't recurse
458 push(@contents, $dom);
459 return 1;
462 return 0;
464 , 'both');
465 $ref->append(@contents);
466 $list[0]{attr}{classes} = ['auto-toc']
467 if ($dom->{content}[0]{content}[0]{tag}
468 eq 'generated');
471 # Check to see if I have any nested sections
472 if (grep($_->{tag} eq 'section',$dom->contents())
473 && (! defined $details->{depth} ||
474 $depth < $details->{depth}-1)) {
475 if ($when eq 'pre') {
476 my $new_bl = new DOM('bullet_list');
477 $li->append($new_bl);
478 push(@list, $new_bl);
480 else {
481 pop(@list);
485 $depth++ if $when eq 'pre';
487 return 0;
489 , 'both') ;
491 # Need to remove all traces of ourselves if the bullet list is empty
492 if ($bl->num_contents() == 0) {
493 $start->splice(0, 1);
494 return;
496 return $bl, @errs;
499 # Processes a docutils.transforms.parts.Sectnum transform.
500 # Auto-numbers the sections in the document.
501 # Arguments: pending DOM, top-level DOM, details hash reference
502 sub Sectnum {
503 my ($dom, $topdom, $details) = @_;
505 # First process the table of contents topic if it exists
506 my ($toc) = grep($_->{tag} eq 'topic' && defined $_->{attr}{classes} &&
507 $_->{attr}{classes}[0] eq 'contents',
508 $topdom->contents());
509 my @list; # Used in closure of sub
510 my $prefix = defined $details->{prefix} ? $details->{prefix} : '';
511 my $suffix = defined $details->{suffix} ? $details->{suffix} : '';
512 my $start = $details->{start} || 1;
513 if (defined $toc) {
514 $toc->Recurse
515 (sub {
516 my($dom, $when) = @_;
517 if ($dom->{tag} eq 'bullet_list') {
518 if ($when eq 'pre') {
519 push(@list, $start-1);
520 $dom->{attr}{classes} = ['auto-toc']
521 if (! defined $details->{depth} ||
522 @list <= $details->{depth});
524 else { pop(@list) };
526 elsif ($dom->{tag} eq 'list_item' && $when eq 'pre') {
527 $list[-1]++;
529 elsif ($dom->{tag} eq 'reference' && $when eq 'pre'
530 && (! defined $details->{depth} ||
531 @list <= $details->{depth})) {
532 my $gen = new DOM('generated', classes=>['sectnum']);
533 $gen->append(newPCDATA DOM($prefix . join('.',@list)
534 . $suffix . ("\xa0"x3)));
535 $dom->prepend($gen);
537 return 0;
539 , 'both') ;
542 # Next process the sections recursively
543 @list = ($start-1);
544 $topdom->Recurse
545 (sub {
546 my($dom, $when) = @_;
547 if ($dom->{tag} eq 'section') {
548 if ($when eq 'pre') {
549 if (! defined $details->{depth} ||
550 @list <= $details->{depth}) {
551 my $title = $dom->{content}[0];
552 $title->{attr}{auto} = 1;
553 $list[-1]++;
554 my $gen = new DOM('generated', classes=>['sectnum']);
555 $gen->append(newPCDATA DOM($prefix . join('.',@list)
556 . $suffix . ("\xa0"x3)));
557 $title->prepend($gen);
559 push(@list, 0);
561 else { pop(@list); }
563 return 0;
565 , 'both') ;
567 return;
570 package docutils::transforms::references;
572 # This package contains routines for transforms of DOM trees
574 # Static global variables
575 use vars qw($NEXT_SYMBOL_FOOTNOTE @FOOTNOTE_SYMBOLS);
577 # Run-time global variables
578 use vars qw($AUTO_FOOTNOTE_REF $LAST_AUTO_FOOTNOTE @AUTO_FOOTNOTES);
580 BEGIN {
581 @FOOTNOTE_SYMBOLS = ("*", chr 0x2020, chr 0x2021, chr 0xa7,
582 chr 0xb6, '#', chr 0x2660, chr 0x2665,
583 chr 0x2666, chr 0x2663);
584 $NEXT_SYMBOL_FOOTNOTE = 0;
587 # Processes a docutils.transforms.references.AutoFootnotes transform.
588 # Computes numbers for autonumbered footnotes.
589 # Arguments: top-level DOM
590 sub AutoFootnotes {
591 my ($dom) = @_;
593 # Compute numbers for autonumbered footnotes
594 $dom->Recurse
595 (sub {
596 my($dom) = @_;
597 my $tag = $dom->{tag};
598 if ($tag eq 'footnote') {
599 if ($dom->{attr}{auto}) {
600 my $label = new DOM('label');
601 $dom->prepend($label);
602 if ($dom->{attr}{auto} eq '1') {
603 while (defined $RST::REFERENCE_DOM{$tag}
604 {++$LAST_AUTO_FOOTNOTE}) { };
605 if (! defined $dom->{attr}{names} &&
606 ! defined $dom->{attr}{dupnames}) {
607 push(@AUTO_FOOTNOTES, $dom);
608 $dom->{attr}{names} = [ $LAST_AUTO_FOOTNOTE ];
609 RST::RegisterName($dom, $dom->{source},
610 $dom->{lineno});
612 $label->append(newPCDATA
613 DOM($LAST_AUTO_FOOTNOTE));
615 else {
616 push(@AUTO_FOOTNOTES, $dom);
617 my $multiplier =
618 int($NEXT_SYMBOL_FOOTNOTE/@FOOTNOTE_SYMBOLS) + 1;
619 my $index =
620 $NEXT_SYMBOL_FOOTNOTE % @FOOTNOTE_SYMBOLS;
621 my $name =
622 ($FOOTNOTE_SYMBOLS[$index]) x $multiplier;
623 $label->append(newPCDATA DOM($name));
624 $NEXT_SYMBOL_FOOTNOTE++;
626 $RST::REFERENCE_DOM{$tag}{$dom->{attr}{names}[0]} = $dom
627 if defined $dom->{attr}{names};
628 $RST::REFERENCE_DOM{$tag}{$dom->{attr}{ids}[0]} = $dom;
631 return 0;
633 , 'pre');
636 # Processes a docutils.transforms.references.IndTargets transform.
637 # Links indirect targets and bare targets to their eventual destination.
638 # Arguments: top-level DOM
639 sub IndTargets {
640 my ($dom) = @_;
642 my @errs;
643 # Process indirect targets
644 $dom->Reshape
645 (sub {
646 my($dom) = @_;
648 my $parent = $dom->parent();
649 my $tag = $dom->{tag};
650 if ($tag eq 'target' && ! $dom->{forward}) {
651 my (%seen, %ind);
652 my $ignores = 'comment|substitution_definition|system_message|pending|image';
653 my $next = $dom;
654 my @chain;
655 while ($next->{tag} eq 'target' &&
656 $next->parent()->{tag} ne 'paragraph' &&
657 (defined $next->{attr}{refname} ||
658 ! grep(/ref(uri|id)/, keys %{$next->{attr}}))) {
659 # This is either an indirect target or a bare target
660 return $dom if $next->{badtarget};
662 if (defined $next->{attr}{refname}) {
663 # This is an indirect target
664 $next->{type} = "Indirect"
665 unless defined $next->{type};
666 # Chain until we come to something not indirect
667 while (defined (my $name = $next->{attr}{refname})
668 && ! $seen{$next})
670 push @chain, $next;
671 my @targets = @{$RST::ALL_TARGET_NAMES{$name}}
672 if defined $RST::ALL_TARGET_NAMES{$name};
673 $seen{$next} = $ind{$next} = $next;
674 if (@targets > 1) {
675 my $errname = defined $dom->{attr}{names} ?
676 qq("$dom->{attr}{names}[0]" ) : '';
677 my $sm = RST::system_message
678 (3, $dom->{source}, $dom->{lineno},
679 qq(Indirect hyperlink target $errname(id="$dom->{attr}{ids}[0]") refers to target "$dom->{attr}{refname}", which is a duplicate, and cannot be used as a unique reference.));
680 push @errs, $sm;
681 # Mark all the seen targets as bad targets
682 foreach my $bad (keys %seen) {
683 $seen{$bad}{badtarget} = $sm;
685 return $dom;
687 $next = $targets[0];
688 return $dom unless $next;
691 else {
692 # This is a chained target. Tie it to what's next
693 my (@ids, @names);
694 my @barechain = ($next);
695 push @chain, $next;
696 unshift @ids, @{$next->{attr}{ids}};
697 unshift @names, @{$next->{attr}{names}} if
698 $next->{attr}{names};
699 $seen{$next} = $next;
700 $next = $next->next($ignores);
701 return $dom unless $next;
702 while ($next->{tag} eq 'target' &&
703 ! grep(/ref(name|uri|id)/, keys %{$next->{attr}}))
705 $seen{$next} = $next;
706 push @chain, $next;
707 unshift @ids, @{$next->{attr}{ids}};
708 unshift @names, @{$next->{attr}{names}}
709 if defined $next->{attr}{names};
710 $next = $next->next($ignores);
711 return $dom unless $next;
713 if ($next->{tag} =~ /^(section|paragraph|target|reference)$/) {
714 push @{$next->{attr}{ids}}, @ids;
715 push @{$next->{attr}{names}}, @names
716 if @names;
717 foreach (@barechain) {
718 &RST::ReregisterName($_, $next);
719 if ($next->{attr}{refname}) {
720 $_->{attr}{refid} = $_->{attr}{ids}[0];
721 delete $_->{attr}{ids};
722 delete $_->{attr}{names};
728 if ($seen{$next}) {
729 my (@refids, @ids);
730 # Generate a problematic for this dom
731 my $prev = $chain[-1];
732 my ($prob, $refid, $id) =
733 RST::problematic($prev->{lit});
734 # Generate the system message
735 my ($first) = grep($ind{$_}, @chain);
736 my $nextname = $next->{attr}{refname};
737 my $sm = RST::system_message
738 (3, $next->{source}, $next->{lineno},
739 qq(Indirect hyperlink target "$first->{attr}{names}[0]" (id="$first->{attr}{ids}[0]") refers to target "$nextname", forming a circular reference.),
740 undef, ids=>[ $refid ],
741 backrefs=>[ $id ]);
742 push @ids, $id;
743 # Mark all the seen targets as bad targets
744 my $tgtrefid = $next->{attr}{ids}[-1];
745 foreach my $bad (keys %seen) {
746 my $baddom = $seen{$bad};
747 $baddom->{badtarget} = $sm;
748 $baddom->{attr}{refid} = $tgtrefid;
749 delete $baddom->{attr}{refname} if $ind{$bad};
751 $prob->{badtarget} = $sm;
752 $sm->{attr}{backrefs} = [ @ids ];
753 push @errs, $sm;
754 # Convert the last target to a problematic
755 %$prev = %$prob;
756 if ($ind{$dom}) {
757 $dom->{attr}{refid} =
758 $dom->{attr}{ids}[0];
759 delete $dom->{attr}{refname};
760 return $dom;
762 # Return a new problematic
763 ($prob, $refid, $id) =
764 RST::problematic($dom->{lit}, $refid);
765 push @{$sm->{attr}{backrefs}}, $id;
766 return $prob;
770 if ($next->{tag} eq 'target' &&
771 defined $next->{attr}{refuri}) {
772 foreach my $prev (keys %seen) {
773 my $prevdom = $seen{$prev};
774 if ($ind{$prev} || defined $prevdom->{attr}{refid}) {
775 delete $prevdom->{attr}{refname};
776 delete $prevdom->{attr}{refid};
777 $prevdom->{attr}{refuri} = $next->{attr}{refuri};
779 else {
780 $prevdom->{attr}{refid} =
781 $prevdom->{attr}{ids}[0];
782 delete $prevdom->{attr}{ids};
783 delete $prevdom->{attr}{names};
786 return $dom;
788 return $dom if $next->{tag} =~ /^(footnote|citation)$/;
789 if ($next->{tag} =~ /^(section|paragraph|target|topic)$/
790 || defined $next->{attr}{refid}) {
791 my $dest = defined $next->{attr}{refid} ?
792 $next->{forward} : $next;
793 my $refid = $next->{attr}{refid} ||
794 $dest->{attr}{ids}[0];
795 # Fill in the refids
796 foreach (@chain) {
797 $_->{forward} = $dest;
798 if ($ind{$_}) {
799 $_->{attr}{refid} = $refid;
800 delete $_->{attr}{refname};
802 else {
803 my $refid = $_->{attr}{ids} &&
804 $_->{attr}{ids}[0] ||
805 $_->{attr}{names} &&
806 $_->{attr}{names}[0];
807 $_->{attr}{refid} = $refid;
808 delete $_->{attr}{ids};
809 delete $_->{attr}{names};
814 return $dom;
816 , 'pre');
818 $dom->append(@errs) if @errs;
821 # Processes a docutils.transforms.references.CitationReferences transform.
822 # Links citation references to their targets.
823 # Arguments: top-level DOM
824 sub CitationReferences {
825 my ($dom) = @_;
827 # Link references to their definitions if they exist
828 my (@errs, $cr);
829 $cr = sub {
830 my($dom) = @_;
832 my $tag = $dom->{tag};
833 if ($tag =~ /^(?:(citation|substitution)_reference)$/) {
834 my $what = $1 eq 'citation' ? $1 :
835 'substitution_definition';
836 my $name = main::FirstDefined($dom->{attr}{names} &&
837 $dom->{attr}{names}[0],
838 $dom->{attr}{refname});
839 my $target = $RST::REFERENCE_DOM{$what}{$name};
840 $target = ($RST::REFERENCE_DOM{"$what.lc"}{lc $name})
841 unless defined $target;
842 if (! defined $target) {
843 my ($prob, $refid, $id) =
844 RST::problematic($dom->{lit});
845 my $emsg = $what eq 'citation' ?
846 'Unknown target name' :
847 'Undefined substitution referenced';
848 push @errs, RST::system_message
849 (3, $dom->{source}, $dom->{lineno},
850 qq($emsg: "$name".), '', ids=>[ $refid ],
851 backrefs=>[ $id ]);
852 return $prob;
854 if ($tag eq 'substitution_reference') {
855 if ($target->{attr}{ltrim} || $target->{attr}{rtrim}) {
856 my $parent = $dom->parent();
857 my $idx = $parent->index($dom);
858 $parent->{content}[$idx-1]{text} =~ s/ *$//
859 if $target->{attr}{ltrim} && $idx > 0 &&
860 $parent->{content}[$idx-1]{tag} eq '#PCDATA';
861 $parent->{content}[$idx+1]{text} =~ s/^ *//
862 if $target->{attr}{rtrim} &&
863 $idx < $parent->num_contents() &&
864 $parent->{content}[$idx+1]{tag} eq '#PCDATA';
866 my @content= $target->contents();
867 my $i;
868 for ($i=0; $i<@content; $i++) {
869 splice(@content, $i, 1, &$cr($content[$i]))
870 if $content[$i]{tag} eq 'substitution_reference';
872 return @content;
874 delete $dom->{attr}{refname};
875 $dom->{attr}{refid} = $target->{attr}{ids}[0];
876 push @{$target->{attr}{backrefs}}, @{$dom->{attr}{ids}};
878 return $dom;
880 $dom->Reshape ($cr, 'pre');
881 $dom->append(@errs) if @errs;
884 # Processes a docutils.transforms.references.FootnoteReferences transform.
885 # Links footnote references to their targets.
886 # Arguments: top-level DOM
887 sub FootnoteReferences {
888 my ($dom) = @_;
890 # Link references to their definitions if they exist
891 my @errs;
892 $dom->Reshape
893 (sub {
894 my($dom) = @_;
895 my $tag = $dom->{tag};
896 if ($tag eq 'footnote_reference' && !$dom->{resolved}) {
897 my $name = main::FirstDefined($dom->{attr}{names} &&
898 $dom->{attr}{names}[0],
899 $dom->{attr}{refname});
900 my $footnote = defined $name ?
901 $RST::REFERENCE_DOM{footnote}{$name} :
902 $AUTO_FOOTNOTES[$AUTO_FOOTNOTE_REF++];
903 if (! defined $footnote) {
904 my ($prob, $refid, $id) =
905 RST::problematic($dom->{lit});
906 push @errs, RST::system_message
907 (3, $dom->{source}, $dom->{lineno},
908 (defined $name ? qq(Unknown target name: "$name".):
909 qq(Too many autonumbered footnote references: only ${\scalar(@AUTO_FOOTNOTES)} corresponding footnotes available.)),
910 '', ids=>[ $refid ], backrefs=>[ $id ]);
911 return $prob;
913 if (defined $footnote->{attr}{dupnames}) {
914 my ($prob, $refid, $id) =
915 RST::problematic($dom->{lit});
916 push @errs, RST::system_message
917 (3, $dom->{source}, $dom->{lineno},
918 (qq(Duplicate target name, cannot be used as a unique reference: "$name".)),
919 '', ids=>[ $refid ], backrefs=>[ $id ]);
920 return $prob;
922 if ($dom->{attr}{auto}) {
923 my $name = $footnote->{content}[0]{content}[0]{text};
924 $dom->append(newPCDATA DOM($name));
926 delete $dom->{attr}{refname};
927 $dom->{attr}{refid} = $footnote->{attr}{ids}[0];
928 push @{$footnote->{attr}{backrefs}}, @{$dom->{attr}{ids}};
929 $dom->{resolved} = 1;
931 return $dom;
933 , 'pre');
934 $dom->append(@errs) if @errs;
937 # Processes a docutils.transforms.references.MarkReferenced transform.
938 # Marks immediate destinations of references as referenced.
939 # Arguments: top-level DOM
940 sub MarkReferenced {
941 my ($dom) = @_;
943 # Mark destinations of references as referenced
944 $dom->Recurse
945 (sub {
946 my($dom) = @_;
947 my $tag = $dom->{tag};
948 if (defined $dom->{attr}{refname}) {
949 my $target;
950 my $name = $dom->{attr}{refname};
951 my @targets = @{$RST::TARGET_NAME{target}{$name}}
952 if defined $RST::TARGET_NAME{target}{$name};
953 if (@targets == 1) {
954 $target = $targets[0];
955 $target->{referenced} = 1;
958 return ;
960 , 'pre');
963 # Processes a docutils.transforms.references.References transform.
964 # Counts anonymous references, links references to their
965 # destinations, produces error messages if the number of anonymous
966 # references is insufficient.
967 # Arguments: top-level DOM
968 sub References {
969 my ($dom) = @_;
971 my $anonymous_refs;
972 # Count how many anonymous references we have
973 $dom->Recurse
974 (sub {
975 my($dom) = @_;
976 $anonymous_refs++
977 if ($dom->{tag} eq 'reference' && $dom->{attr}{anonymous});
978 return 0;
980 , 'pre');
981 my $last_anonymous_target = 0;
982 my $anonymous_mismatch_id;
983 my @anonymous_mismatch_refids;
984 my @errs;
985 # Link references to their definitions if they exist
986 $dom->Reshape
987 (sub {
988 my($dom) = @_;
989 my $tag = $dom->{tag};
990 if ($tag eq 'reference' && ! defined $dom->{attr}{refuri} &&
991 ! defined $dom->{attr}{refid}) {
992 my $target;
993 my $name = $dom->{attr}{refname};
994 if (defined $name) {
995 my @targets = @{$RST::TARGET_NAME{target}{$name}}
996 if defined $RST::TARGET_NAME{target}{$name};
997 if (@targets > 1) {
998 my ($prob, $refid, $id) =
999 RST::problematic($dom->{lit});
1001 push @errs, RST::system_message
1002 (3, $dom->{source}, $dom->{lineno},
1003 qq(Duplicate target name, cannot be used as a unique reference: "$name".),
1004 undef, backrefs=>[ $id ], ids=>[ $refid ]);
1005 return $prob;
1007 $target = $targets[0];
1008 if (! defined $target &&
1009 ! defined $RST::ALL_TARGET_IDS{$name}[0]) {
1010 my ($prob, $refid, $id) =
1011 RST::problematic($dom->{lit});
1012 push @errs, RST::system_message
1013 (3, $dom->{source}, $dom->{lineno},
1014 qq(Unknown target name: "$name".),
1015 '', ids=> [ $refid ], backrefs=>[ $id ]);
1016 return $prob;
1019 elsif ($dom->{attr}{anonymous}) {
1020 if ($anonymous_refs > @RST::ANONYMOUS_TARGETS) {
1021 $anonymous_mismatch_id = RST::Id()
1022 if ! defined $anonymous_mismatch_id;
1023 my ($prob, $refid, $id) =
1024 RST::problematic($dom->{lit},
1025 $anonymous_mismatch_id);
1026 push(@anonymous_mismatch_refids, $id);
1027 return $prob;
1029 $target =
1030 $RST::ANONYMOUS_TARGETS[$last_anonymous_target++];
1032 while (defined $target) {
1033 if ($target->{badtarget}) {
1034 my $sm = $target->{badtarget};
1035 my ($prob, $refid, $id) =
1036 RST::problematic($dom->{lit},
1037 $sm->{attr}{ids}[0]);
1038 push @{$sm->{attr}{backrefs}}, $id;
1039 $sm->{attr}{ids} = [ $refid ];
1040 return $prob;
1042 my $dest = $target->{forward} || $target;
1043 if ($dest->{tag} eq 'target' &&
1044 defined $dest->{attr}{refuri}) {
1045 $target->{type} = "External"
1046 unless defined $target->{type};
1047 delete $dom->{attr}{refname};
1048 $dom->{attr}{refuri} = $dest->{attr}{refuri};
1050 elsif ($target->{forward}) {
1051 delete $dom->{attr}{refname};
1052 $dom->{attr}{refid} = $target->{attr}{refid};
1054 elsif (defined $target->{attr}{refid}) {
1055 $target->{type} = "Internal"
1056 unless defined $target->{type};
1057 my @targets = @{$RST::ALL_TARGET_IDS
1058 {$target->{attr}{refid}}};
1059 $target = $targets[0];
1060 next;
1062 elsif (defined $target->{attr}{ids}) {
1063 my $refid =
1064 RST::NormalizeId($dom->{attr}{refname});
1065 $dom->{attr}{refid} =
1066 grep($_ eq $refid, @{$target->{attr}{ids}}) ?
1067 $refid : $target->{attr}{ids}[0];
1068 # $dom->{attr}{refid} =
1069 # RST::NormalizeId($dom->{attr}{refname});
1070 delete $dom->{attr}{refname};
1072 undef $target;
1075 return $dom;
1077 , 'pre');
1079 # Produce messages if there aren't enough anonymous hyperlink targets
1080 if (defined $anonymous_mismatch_id) {
1081 my $sm = RST::system_message
1082 (3, $dom->{attr}{source}, undef,
1083 qq(Anonymous hyperlink mismatch: $anonymous_refs references but ${\scalar(@RST::ANONYMOUS_TARGETS)} targets.\nSee "backrefs" attribute for IDs.),
1084 '', ids=>[ $anonymous_mismatch_id ],
1085 backrefs=>[ @anonymous_mismatch_refids ]);
1086 delete $sm->{attr}{line};
1087 $dom->append($sm);
1090 $dom->append(@errs) if @errs;
1093 # Processes a docutils.transforms.references.Unreferenced transform.
1094 # Produces messages for unreferenced targets.
1095 # Arguments: top-level DOM
1096 sub Unreferenced {
1097 my ($dom) = @_;
1099 # Produce messages for unreferenced targets
1100 my @errs;
1101 $dom->Reshape
1102 (sub {
1103 my($dom) = @_;
1104 my $tag = $dom->{tag};
1105 if ($tag eq 'target' && ! $dom->{referenced} &&
1106 ! $dom->{attr}{anonymous} && ! $dom->{attr}{dupnames}) {
1107 my $name =
1108 defined $dom->{attr}{names} && $dom->{attr}{names}[0] ||
1109 defined $dom->{attr}{dupnames} &&
1110 $dom->{attr}{dupnames}[0] || $dom->{attr}{refid};
1111 my $id = defined $name ? qq("$name") :
1112 qq(id="$dom->{attr}{ids}[0]");
1113 push @errs, RST::system_message
1114 (1, $dom->{source}, $dom->{lineno},
1115 qq(Hyperlink target $id is not referenced.));
1116 return $dom;
1118 return $dom;
1120 , 'pre');
1121 $dom->append(@errs);
1124 # Processes a docutils.transforms.references.TargetNotes transform.
1125 # Constructs a list of external references and creates footnotes
1126 # pointing to them.
1127 # Arguments: pending DOM, top-level DOM, details hash reference
1128 sub TargetNotes {
1129 my ($dom, $topdom, $details) = @_;
1131 my @targets; # Used in closure of sub
1132 # Construct the list of external references.
1133 $topdom->Recurse
1134 (sub {
1135 my($dom) = @_;
1136 my $tag = $dom->{tag};
1137 push (@targets, $dom)
1138 if $tag eq 'target' && defined $dom->{attr}{refuri};
1139 return 0;
1140 }) ;
1142 # Create the footnotes
1143 my @doms;
1144 my %footnotes;
1145 foreach (@targets) {
1146 my $id = RST::Id();
1147 $footnotes{$_->{attr}{names}[0]} = $id;
1148 my $dom = new DOM('footnote', auto=>1, ids=>[ $id ],
1149 names=>[ "TARGET_NOTE: $id" ]);
1150 my $para = new DOM('paragraph');
1151 $dom->append($para);
1152 my $ref = new DOM('reference', refuri=>$_->{attr}{refuri});
1153 $para->append($ref);
1154 $ref->append(newPCDATA DOM($_->{attr}{refuri}));
1155 push(@doms, $dom);
1158 # Insert the footnote references
1159 $topdom->Reshape
1160 (sub {
1161 my($dom) = @_;
1162 my $tag = $dom->{tag};
1163 if ($tag eq 'reference' &&
1164 defined $dom->{attr}{refname} &&
1165 defined $footnotes{$dom->{attr}{refname}}) {
1166 my $refname = $footnotes{$dom->{attr}{refname}};
1167 my $fr = new DOM('footnote_reference', auto=>1,
1168 ids=> [ RST::Id() ],
1169 refname=>"TARGET_NOTE: $refname");
1170 return ($dom, newPCDATA DOM(' '), $fr);
1172 return $dom;
1173 }) ;
1175 return @doms;
1178 package docutils::transforms::universal;
1180 # This package contains routines for transforms of DOM trees
1182 # Processes a docutils.transforms.universal.Decorations transform.
1183 # Adds the "View docuemnt source", "Generated on" and "Generated by"
1184 # decorations to the end of the document.
1185 # Arguments: top-level DOM
1186 sub Decorations {
1187 my ($topdom) = @_;
1189 my ($dec) = grep $_->{tag} eq 'decoration', $topdom->contents();
1190 return if defined $dec && ($dec->{content}[0]{tag} eq 'footer' ||
1191 $dec->num_contents() > 1);
1192 my $para = new DOM('paragraph');
1193 my $source_link = main::FirstDefined($main::opt_D{source_link}, 1);
1194 if ($source_link) {
1195 my $source_url = main::FirstDefined($main::opt_D{source_url},
1196 $topdom->{attr}{source});
1197 my $ref = new DOM('reference', refuri=>$source_url);
1198 $ref->append(newPCDATA DOM('View document source'));
1199 $para->append($ref);
1200 $para->append(newPCDATA DOM(".\n"));
1202 my $time = main::FirstDefined($main::opt_D{time}, 1);
1203 my $date = main::FirstDefined($main::opt_D{date}, 0);
1204 if ($date || $time) {
1205 my $format = "%Y/%m/%d" . ($time ? " %H:%M:%S %Z" : "");
1206 use POSIX;
1207 my $date = POSIX::strftime($format, localtime);
1208 $para->append(newPCDATA DOM("Generated on: $date.\n"));
1210 my $generator = main::FirstDefined($main::opt_D{generator}, 1);
1211 if ($generator) {
1212 my $ref = new DOM('reference', refuri=>
1213 'http://docutils.sourceforge.net/rst.html');
1214 $ref->append(newPCDATA DOM("reStructuredText"));
1215 $para->append(newPCDATA DOM("Generated by $main::TOOL_ID from "),
1216 $ref,
1217 newPCDATA DOM(" source.\n"));
1220 if ($para->num_contents()) {
1221 my $dec = new DOM('decoration');
1222 my $footer = new DOM('footer');
1223 $dec->append($footer);
1224 $footer->append($para);
1225 # Decoration needs to be appended before the document model
1226 # starts, i.e., after the latest of title or subtitle.
1227 my $i;
1228 for ($i=0; $i<$topdom->num_contents(); $i++) {
1229 if ($topdom->{content}[$i]{tag} !~ /title|docinfo/) {
1230 $topdom->splice($i, 0, $dec);
1231 last;
1237 # Processes a docutils.transforms.universal.EmptyTopics transform.
1238 # Removes any topics that have only a header in their contents.
1239 # Arguments: top-level DOM
1240 sub EmptyTopics {
1241 my ($dom) = @_;
1243 $dom->Reshape
1244 (sub {
1245 my($dom) = @_;
1246 return if $dom->{tag} eq 'topic' &&
1247 ($dom->num_contents() == 0 ||
1248 $dom->{content}[-1]{tag} eq 'title');
1249 return $dom;
1253 # Processes a docutils.transforms.universal.Messages transform.
1254 # Moves system messages at the end into "Docutils System Messages" section.
1255 # Arguments: top-level DOM
1256 sub Messages {
1257 my ($dom) = @_;
1259 # Move system messages at the end to a section
1260 my @SYSTEM_MESSAGES;
1261 $dom->Reshape
1262 (sub {
1263 my($dom) = @_;
1264 push (@SYSTEM_MESSAGES, $dom)
1265 if ($dom->{tag} eq 'system_message' &&
1266 $dom->{attr}{level} >= $main::opt_D{report});
1267 return $dom->{tag} ne 'system_message' ? ($dom) : ();
1271 if (@SYSTEM_MESSAGES > 0) {
1272 my $errsec = new DOM('section', classes=>['system-messages']);
1273 $dom->append($errsec);
1274 my $title = new DOM('title');
1275 $errsec->append($title);
1276 $title->append(newPCDATA DOM('Docutils System Messages'));
1277 $errsec->append(@SYSTEM_MESSAGES);
1281 # Processes a docutils.transforms.universal.ScoopMessages transform.
1282 # Moves system messages from anywhere in the DOM tree to the end of
1283 # the top-level DOM.
1284 # Arguments: top-level DOM
1285 sub ScoopMessages {
1286 my ($dom) = @_;
1288 # Move system messages into end of top dom's contents
1289 my @SYSTEM_MESSAGES;
1290 $dom->Reshape
1291 (sub {
1292 my($dom) = @_;
1293 if ($dom->{tag} eq 'system_message') {
1294 if (defined $dom->{attr}{ids}) {
1295 push(@SYSTEM_MESSAGES, $dom);
1296 return;
1299 return $dom;
1301 $dom->append(@SYSTEM_MESSAGES);
1304 # Processes a docutils.transforms.universal.Transitions transform.
1305 # Moves transitions at end of sections to top level and creates error
1306 # messages for incorrect transitions.
1307 # Arguments: top-level DOM
1308 sub Transitions {
1309 my ($dom) = @_;
1311 # Move system messages at the end to a section
1312 my @errs;
1313 $dom->Reshape
1314 (sub {
1315 my($dom) = @_;
1316 if ($dom->{tag} eq 'transition') {
1317 my $domparent = $dom->parent();
1318 my $idx = $domparent->index($dom);
1319 my @doms;
1320 push @doms, RST::system_message
1321 (3, $dom->{source}, $dom->{lineno},
1322 "Document or section may not begin with a transition.")
1323 if $idx == 0 ||
1324 $idx == 1 && $domparent->{content}[0]{tag} eq 'title';
1325 my $next = $dom->next();
1326 if ($next && ($next->parent() || 'NONE') != $domparent) {
1327 $next->{transition} = $dom;
1328 return @doms;
1330 push @doms, $dom;
1331 if (! defined $next) {
1332 push @doms, RST::system_message
1333 (3, $dom->{source}, $dom->{lineno},
1334 "Document may not end with a transition.");
1336 elsif ($next->{tag} eq 'transition') {
1337 push @doms, RST::system_message
1338 (3, $next->{source}, $next->{lineno},
1339 "At least one body element must separate transitions; adjacent transitions are not allowed.");
1341 return @doms;
1343 elsif (my $t = $dom->{transition}) {
1344 delete $dom->{transition};
1345 return ($t, $dom);
1347 return $dom;
1349 , 'post');
1351 $dom->append(@errs);