001package org.apache.maven.doxia.module.fo; 002 003/* 004 * Licensed to the Apache Software Foundation (ASF) under one 005 * or more contributor license agreements. See the NOTICE file 006 * distributed with this work for additional information 007 * regarding copyright ownership. The ASF licenses this file 008 * to you under the Apache License, Version 2.0 (the 009 * "License"); you may not use this file except in compliance 010 * with the License. You may obtain a copy of the License at 011 * 012 * http://www.apache.org/licenses/LICENSE-2.0 013 * 014 * Unless required by applicable law or agreed to in writing, 015 * software distributed under the License is distributed on an 016 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 017 * KIND, either express or implied. See the License for the 018 * specific language governing permissions and limitations 019 * under the License. 020 */ 021 022import java.io.IOException; 023import java.io.Writer; 024 025import java.util.Calendar; 026import java.util.Date; 027import java.util.LinkedList; 028import java.util.List; 029import java.util.Locale; 030import java.util.ResourceBundle; 031import java.util.Stack; 032 033import javax.swing.text.MutableAttributeSet; 034import javax.swing.text.html.HTML.Tag; 035 036import org.apache.maven.doxia.document.DocumentCover; 037import org.apache.maven.doxia.document.DocumentMeta; 038import org.apache.maven.doxia.document.DocumentModel; 039import org.apache.maven.doxia.document.DocumentTOC; 040import org.apache.maven.doxia.document.DocumentTOCItem; 041import org.apache.maven.doxia.sink.SinkEventAttributes; 042import org.apache.maven.doxia.sink.impl.SinkEventAttributeSet; 043import org.apache.maven.doxia.util.DoxiaUtils; 044import org.apache.maven.doxia.util.HtmlTools; 045 046import org.codehaus.plexus.util.StringUtils; 047 048/** 049 * A Doxia Sink that produces an aggregated FO model. The usage is similar to the following: 050 * <pre> 051 * FoAggregateSink sink = new FoAggregateSink( writer ); 052 * sink.setDocumentModel( documentModel ); 053 * sink.beginDocument(); 054 * sink.coverPage(); 055 * sink.toc(); 056 * ... 057 * sink.endDocument(); 058 * </pre> 059 * <b>Note</b>: the documentModel object contains several 060 * <a href="https://maven.apache.org/doxia/doxia/doxia-core/document.html">document metadata</a>, but only a few 061 * of them are used in this sink (i.e. author, confidential, date and title), the others are ignored. 062 * 063 * @author ltheussl 064 * @since 1.1 065 */ 066public class FoAggregateSink 067 extends FoSink 068{ 069 /** 070 * No Table Of Content. 071 * 072 * @see #setDocumentModel(DocumentModel, int) 073 */ 074 public static final int TOC_NONE = 0; 075 076 /** 077 * Table Of Content at the start of the document. 078 * 079 * @see #setDocumentModel(DocumentModel, int) 080 */ 081 public static final int TOC_START = 1; 082 083 /** 084 * Table Of Content at the end of the document. 085 * 086 * @see #setDocumentModel(DocumentModel, int) 087 */ 088 public static final int TOC_END = 2; 089 090 // TODO: make configurable 091 private static final String COVER_HEADER_HEIGHT = "1.5in"; 092 093 /** 094 * The document model to be used by this sink. 095 */ 096 private DocumentModel docModel; 097 098 /** 099 * Counts the current chapter level. 100 */ 101 private int chapter = 0; 102 103 /** 104 * Name of the source file of the current document, relative to the source root. 105 */ 106 private String docName; 107 108 /** 109 * Title of the chapter, used in the page header. 110 */ 111 private String docTitle = ""; 112 113 /** 114 * Content in head is ignored in aggregated documents. 115 */ 116 private boolean ignoreText; 117 118 /** 119 * Current position of the TOC, see {@link #TOC_POSITION} 120 */ 121 private int tocPosition; 122 123 /** 124 * Used to get the current position in the TOC. 125 */ 126 private final Stack<NumberedListItem> tocStack = new Stack<>(); 127 128 /** 129 * Constructor. 130 * 131 * @param writer The writer for writing the result. 132 */ 133 public FoAggregateSink( Writer writer ) 134 { 135 super( writer ); 136 } 137 138 /** 139 * {@inheritDoc} 140 */ 141 public void head() 142 { 143 head( null ); 144 } 145 146 /** {@inheritDoc} */ 147 public void head( SinkEventAttributes attributes ) 148 { 149 init(); 150 151 ignoreText = true; 152 } 153 154 /** 155 * {@inheritDoc} 156 */ 157 public void head_() 158 { 159 ignoreText = false; 160 writeEOL(); 161 } 162 163 /** 164 * {@inheritDoc} 165 */ 166 public void title() 167 { 168 title( null ); 169 } 170 171 /** {@inheritDoc} */ 172 public void title( SinkEventAttributes attributes ) 173 { 174 // ignored 175 } 176 177 /** 178 * {@inheritDoc} 179 */ 180 public void title_() 181 { 182 // ignored 183 } 184 185 /** 186 * {@inheritDoc} 187 */ 188 public void author() 189 { 190 author( null ); 191 } 192 193 /** {@inheritDoc} */ 194 public void author( SinkEventAttributes attributes ) 195 { 196 // ignored 197 } 198 199 /** 200 * {@inheritDoc} 201 */ 202 public void author_() 203 { 204 // ignored 205 } 206 207 /** 208 * {@inheritDoc} 209 */ 210 public void date() 211 { 212 date( null ); 213 } 214 215 /** {@inheritDoc} */ 216 public void date( SinkEventAttributes attributes ) 217 { 218 // ignored 219 } 220 221 /** 222 * {@inheritDoc} 223 */ 224 public void date_() 225 { 226 // ignored 227 } 228 229 /** 230 * {@inheritDoc} 231 */ 232 public void body() 233 { 234 body( null ); 235 } 236 237 /** {@inheritDoc} */ 238 public void body( SinkEventAttributes attributes ) 239 { 240 chapter++; 241 242 resetSectionCounter(); 243 244 startPageSequence( getHeaderText(), getFooterText() ); 245 246 if ( docName == null ) 247 { 248 getLog().warn( "No document root specified, local links will not be resolved correctly!" ); 249 } 250 else 251 { 252 writeStartTag( BLOCK_TAG, "" ); 253 } 254 255 } 256 257 /** 258 * {@inheritDoc} 259 */ 260 public void body_() 261 { 262 writeEOL(); 263 writeEndTag( BLOCK_TAG ); 264 writeEndTag( FLOW_TAG ); 265 writeEndTag( PAGE_SEQUENCE_TAG ); 266 267 // reset document name 268 docName = null; 269 } 270 271 /** 272 * Sets the title of the current document. This is used as a chapter title in the page header. 273 * 274 * @param title the title of the current document. 275 */ 276 public void setDocumentTitle( String title ) 277 { 278 this.docTitle = title; 279 280 if ( title == null ) 281 { 282 this.docTitle = ""; 283 } 284 } 285 286 /** 287 * Sets the name of the current source document, relative to the source root. 288 * Used to resolve links to other source documents. 289 * 290 * @param name the name for the current document. 291 */ 292 public void setDocumentName( String name ) 293 { 294 this.docName = getIdName( name ); 295 } 296 297 /** 298 * Sets the DocumentModel to be used by this sink. The DocumentModel provides all the meta-information 299 * required to render a document, eg settings for the cover page, table of contents, etc. 300 * <br> 301 * By default, a TOC will be added at the beginning of the document. 302 * 303 * @param model the DocumentModel. 304 * @see #setDocumentModel(DocumentModel, int) 305 * @see #TOC_START 306 */ 307 public void setDocumentModel( DocumentModel model ) 308 { 309 setDocumentModel( model, TOC_START ); 310 } 311 312 /** 313 * Sets the DocumentModel to be used by this sink. The DocumentModel provides all the meta-information 314 * required to render a document, eg settings for the cover page, table of contents, etc. 315 * 316 * @param model the DocumentModel, could be null. 317 * @param tocPos should be one of these values: {@link #TOC_NONE}, {@link #TOC_START} and {@link #TOC_END}. 318 * @since 1.1.2 319 */ 320 public void setDocumentModel( DocumentModel model, int tocPos ) 321 { 322 this.docModel = model; 323 if ( !( tocPos == TOC_NONE || tocPos == TOC_START || tocPos == TOC_END ) ) 324 { 325 if ( getLog().isDebugEnabled() ) 326 { 327 getLog().debug( "Unrecognized value for tocPosition: " + tocPos + ", using no toc." ); 328 } 329 tocPos = TOC_NONE; 330 } 331 this.tocPosition = tocPos; 332 333 if ( this.docModel != null && this.docModel.getToc() != null && this.tocPosition != TOC_NONE ) 334 { 335 DocumentTOCItem tocItem = new DocumentTOCItem(); 336 tocItem.setName( this.docModel.getToc().getName() ); 337 tocItem.setRef( "./toc" ); 338 List<DocumentTOCItem> items = new LinkedList<>(); 339 if ( this.tocPosition == TOC_START ) 340 { 341 items.add( tocItem ); 342 } 343 items.addAll( this.docModel.getToc().getItems() ); 344 if ( this.tocPosition == TOC_END ) 345 { 346 items.add( tocItem ); 347 } 348 349 this.docModel.getToc().setItems( items ); 350 } 351 } 352 353 /** 354 * Translates the given name to a usable id. 355 * Prepends "./" and strips any extension. 356 * 357 * @param name the name for the current document. 358 * @return String 359 */ 360 private String getIdName( String name ) 361 { 362 if ( StringUtils.isEmpty( name ) ) 363 { 364 getLog().warn( "Empty document reference, links will not be resolved correctly!" ); 365 return ""; 366 } 367 368 String idName = name.replace( '\\', '/' ); 369 370 // prepend "./" and strip extension 371 if ( !idName.startsWith( "./" ) ) 372 { 373 idName = "./" + idName; 374 } 375 376 if ( idName.substring( 2 ).lastIndexOf( "." ) != -1 ) 377 { 378 idName = idName.substring( 0, idName.lastIndexOf( "." ) ); 379 } 380 381 while ( idName.contains( "//" ) ) 382 { 383 idName = StringUtils.replace( idName, "//", "/" ); 384 } 385 386 return idName; 387 } 388 389 // ----------------------------------------------------------------------- 390 // 391 // ----------------------------------------------------------------------- 392 393 /** {@inheritDoc} */ 394 public void figureGraphics( String name ) 395 { 396 figureGraphics( name, null ); 397 } 398 399 /** {@inheritDoc} */ 400 public void figureGraphics( String src, SinkEventAttributes attributes ) 401 { 402 String anchor = src; 403 404 while ( anchor.startsWith( "./" ) ) 405 { 406 anchor = anchor.substring( 2 ); 407 } 408 409 if ( anchor.startsWith( "../" ) && docName != null ) 410 { 411 anchor = resolveLinkRelativeToBase( anchor ); 412 } 413 414 super.figureGraphics( anchor, attributes ); 415 } 416 417 /** {@inheritDoc} */ 418 public void anchor( String name ) 419 { 420 anchor( name, null ); 421 } 422 423 /** {@inheritDoc} */ 424 public void anchor( String name, SinkEventAttributes attributes ) 425 { 426 if ( name == null ) 427 { 428 throw new NullPointerException( "Anchor name cannot be null!" ); 429 } 430 431 String anchor = name; 432 433 if ( !DoxiaUtils.isValidId( anchor ) ) 434 { 435 anchor = DoxiaUtils.encodeId( name, true ); 436 437 String msg = "Modified invalid anchor name: '" + name + "' to '" + anchor + "'"; 438 logMessage( "modifiedLink", msg ); 439 } 440 441 anchor = "#" + anchor; 442 443 if ( docName != null ) 444 { 445 anchor = docName + anchor; 446 } 447 448 writeStartTag( INLINE_TAG, "id", anchor ); 449 } 450 451 /** {@inheritDoc} */ 452 public void link( String name ) 453 { 454 link( name, null ); 455 } 456 457 /** {@inheritDoc} */ 458 public void link( String name, SinkEventAttributes attributes ) 459 { 460 if ( name == null ) 461 { 462 throw new NullPointerException( "Link name cannot be null!" ); 463 } 464 465 if ( DoxiaUtils.isExternalLink( name ) ) 466 { 467 // external links 468 writeStartTag( BASIC_LINK_TAG, "external-destination", HtmlTools.escapeHTML( name ) ); 469 writeStartTag( INLINE_TAG, "href.external" ); 470 return; 471 } 472 473 while ( name.contains( "//" ) ) 474 { 475 name = StringUtils.replace( name, "//", "/" ); 476 } 477 478 if ( DoxiaUtils.isInternalLink( name ) ) 479 { 480 // internal link (ie anchor is in the same source document) 481 String anchor = name.substring( 1 ); 482 483 if ( !DoxiaUtils.isValidId( anchor ) ) 484 { 485 String tmp = anchor; 486 anchor = DoxiaUtils.encodeId( anchor, true ); 487 488 String msg = "Modified invalid anchor name: '" + tmp + "' to '" + anchor + "'"; 489 logMessage( "modifiedLink", msg ); 490 } 491 492 if ( docName != null ) 493 { 494 anchor = docName + "#" + anchor; 495 } 496 497 writeStartTag( BASIC_LINK_TAG, "internal-destination", HtmlTools.escapeHTML( anchor ) ); 498 writeStartTag( INLINE_TAG, "href.internal" ); 499 } 500 else if ( name.startsWith( "../" ) ) 501 { 502 // local link (ie anchor is not in the same source document) 503 504 if ( docName == null ) 505 { 506 // can't resolve link without base, fop will issue a warning 507 writeStartTag( BASIC_LINK_TAG, "internal-destination", HtmlTools.escapeHTML( name ) ); 508 writeStartTag( INLINE_TAG, "href.internal" ); 509 510 return; 511 } 512 513 String anchor = resolveLinkRelativeToBase( chopExtension( name ) ); 514 515 writeStartTag( BASIC_LINK_TAG, "internal-destination", HtmlTools.escapeHTML( anchor ) ); 516 writeStartTag( INLINE_TAG, "href.internal" ); 517 } 518 else 519 { 520 // local link (ie anchor is not in the same source document) 521 522 String anchor = name; 523 524 if ( anchor.startsWith( "./" ) ) 525 { 526 this.link( anchor.substring( 2 ) ); 527 return; 528 } 529 530 anchor = chopExtension( anchor ); 531 532 String base = docName.substring( 0, docName.lastIndexOf( "/" ) ); 533 anchor = base + "/" + anchor; 534 535 writeStartTag( BASIC_LINK_TAG, "internal-destination", HtmlTools.escapeHTML( anchor ) ); 536 writeStartTag( INLINE_TAG, "href.internal" ); 537 } 538 } 539 540 // only call this if docName != null !!! 541 private String resolveLinkRelativeToBase( String name ) 542 { 543 String anchor = name; 544 545 String base = docName.substring( 0, docName.lastIndexOf( "/" ) ); 546 547 if ( base.contains( "/" ) ) 548 { 549 while ( anchor.startsWith( "../" ) ) 550 { 551 base = base.substring( 0, base.lastIndexOf( "/" ) ); 552 553 anchor = anchor.substring( 3 ); 554 555 if ( base.lastIndexOf( "/" ) == -1 ) 556 { 557 while ( anchor.startsWith( "../" ) ) 558 { 559 anchor = anchor.substring( 3 ); 560 } 561 break; 562 } 563 } 564 } 565 566 return base + "/" + anchor; 567 } 568 569 private String chopExtension( String name ) 570 { 571 String anchor = name; 572 573 int dot = anchor.lastIndexOf( "." ); 574 575 if ( dot != -1 && dot != anchor.length() && anchor.charAt( dot + 1 ) != '/' ) 576 { 577 int hash = anchor.indexOf( "#", dot ); 578 579 if ( hash != -1 ) 580 { 581 int dot2 = anchor.indexOf( ".", hash ); 582 583 if ( dot2 != -1 ) 584 { 585 anchor = 586 anchor.substring( 0, dot ) + "#" + HtmlTools.encodeId( anchor.substring( hash + 1, dot2 ) ); 587 } 588 else 589 { 590 anchor = anchor.substring( 0, dot ) + "#" + HtmlTools.encodeId( 591 anchor.substring( hash + 1 ) ); 592 } 593 } 594 else 595 { 596 anchor = anchor.substring( 0, dot ); 597 } 598 } 599 600 return anchor; 601 } 602 603 // ---------------------------------------------------------------------- 604 // 605 // ---------------------------------------------------------------------- 606 607 /** {@inheritDoc} */ 608 @Override 609 protected void writeStartTag( Tag tag, String attributeId ) 610 { 611 if ( !ignoreText ) 612 { 613 super.writeStartTag( tag, attributeId ); 614 } 615 } 616 617 /** {@inheritDoc} */ 618 @Override 619 protected void writeStartTag( Tag tag, String id, String name ) 620 { 621 if ( !ignoreText ) 622 { 623 super.writeStartTag( tag, id, name ); 624 } 625 } 626 627 /** {@inheritDoc} */ 628 @Override 629 protected void writeEndTag( Tag t ) 630 { 631 if ( !ignoreText ) 632 { 633 super.writeEndTag( t ); 634 } 635 } 636 637 /** {@inheritDoc} */ 638 @Override 639 protected void writeEmptyTag( Tag tag, String attributeId ) 640 { 641 if ( !ignoreText ) 642 { 643 super.writeEmptyTag( tag, attributeId ); 644 } 645 } 646 647 /** {@inheritDoc} */ 648 @Override 649 protected void write( String text ) 650 { 651 if ( !ignoreText ) 652 { 653 super.write( text ); 654 } 655 } 656 657 /** {@inheritDoc} */ 658 @Override 659 protected void writeln( String text ) 660 { 661 if ( !ignoreText ) 662 { 663 super.writeln( text ); 664 } 665 } 666 667 /** {@inheritDoc} */ 668 @Override 669 protected void content( String text ) 670 { 671 if ( !ignoreText ) 672 { 673 super.content( text ); 674 } 675 } 676 677 /** 678 * Writes EOL. 679 */ 680 protected void newline() 681 { 682 if ( !ignoreText ) 683 { 684 writeEOL(); 685 } 686 } 687 688 /** 689 * Starts a page sequence, depending on the current chapter. 690 * 691 * @param headerText The text to write in the header, if null, nothing is written. 692 * @param footerText The text to write in the footer, if null, nothing is written. 693 */ 694 protected void startPageSequence( String headerText, String footerText ) 695 { 696 if ( chapter == 1 ) 697 { 698 startPageSequence( "0", headerText, footerText ); 699 } 700 else 701 { 702 startPageSequence( "auto", headerText, footerText ); 703 } 704 } 705 706 /** 707 * Returns the text to write in the header of each page. 708 * 709 * @return String 710 */ 711 protected String getHeaderText() 712 { 713 return chapter + " " + docTitle; 714 } 715 716 /** 717 * Returns the text to write in the footer of each page. 718 * 719 * @return String 720 */ 721 protected String getFooterText() 722 { 723 int actualYear; 724 String add = " • " + getBundle( Locale.US ).getString( "footer.rights" ); 725 String companyName = ""; 726 727 if ( docModel != null && docModel.getMeta() != null && docModel.getMeta().isConfidential() ) 728 { 729 add = add + " • " + getBundle( Locale.US ).getString( "footer.confidential" ); 730 } 731 732 if ( docModel != null && docModel.getCover() != null && docModel.getCover().getCompanyName() != null ) 733 { 734 companyName = docModel.getCover().getCompanyName(); 735 } 736 737 if ( docModel != null && docModel.getMeta() != null && docModel.getMeta().getDate() != null ) 738 { 739 Calendar date = Calendar.getInstance(); 740 date.setTime( docModel.getMeta().getDate() ); 741 actualYear = date.get( Calendar.YEAR ); 742 } 743 else 744 { 745 actualYear = Calendar.getInstance().get( Calendar.YEAR ); 746 } 747 748 return "©" + actualYear + ", " + escaped( companyName, false ) + add; 749 } 750 751 /** {@inheritDoc} */ 752 @Override 753 protected String getChapterString() 754 { 755 return chapter + "."; 756 } 757 758 /** {@inheritDoc} */ 759 @Override 760 protected void regionBefore( String headerText ) 761 { 762 writeStartTag( STATIC_CONTENT_TAG, "flow-name", "xsl-region-before" ); 763 writeln( "<fo:table table-layout=\"fixed\" width=\"100%\" >" ); 764 writeEmptyTag( TABLE_COLUMN_TAG, "column-width", "5.625in" ); 765 writeEmptyTag( TABLE_COLUMN_TAG, "column-width", "0.625in" ); 766 writeStartTag( TABLE_BODY_TAG, "" ); 767 writeStartTag( TABLE_ROW_TAG, "" ); 768 writeStartTag( TABLE_CELL_TAG, "" ); 769 writeStartTag( BLOCK_TAG, "header.style" ); 770 771 if ( headerText != null ) 772 { 773 write( headerText ); 774 } 775 776 writeEndTag( BLOCK_TAG ); 777 writeEndTag( TABLE_CELL_TAG ); 778 writeStartTag( TABLE_CELL_TAG, "" ); 779 writeStartTag( BLOCK_TAG, "page.number" ); 780 writeEmptyTag( PAGE_NUMBER_TAG, "" ); 781 writeEndTag( BLOCK_TAG ); 782 writeEndTag( TABLE_CELL_TAG ); 783 writeEndTag( TABLE_ROW_TAG ); 784 writeEndTag( TABLE_BODY_TAG ); 785 writeEndTag( TABLE_TAG ); 786 writeEndTag( STATIC_CONTENT_TAG ); 787 } 788 789 /** {@inheritDoc} */ 790 @Override 791 protected void regionAfter( String footerText ) 792 { 793 writeStartTag( STATIC_CONTENT_TAG, "flow-name", "xsl-region-after" ); 794 writeStartTag( BLOCK_TAG, "footer.style" ); 795 796 if ( footerText != null ) 797 { 798 write( footerText ); 799 } 800 801 writeEndTag( BLOCK_TAG ); 802 writeEndTag( STATIC_CONTENT_TAG ); 803 } 804 805 /** {@inheritDoc} */ 806 @Override 807 protected void chapterHeading( String headerText, boolean chapterNumber ) 808 { 809 if ( docName == null ) 810 { 811 getLog().warn( "No document root specified, local links will not be resolved correctly!" ); 812 writeStartTag( BLOCK_TAG, "" ); 813 } 814 else 815 { 816 writeStartTag( BLOCK_TAG, "id", docName ); 817 } 818 819 writeStartTag( LIST_BLOCK_TAG, "" ); 820 writeStartTag( LIST_ITEM_TAG, "" ); 821 writeln( "<fo:list-item-label end-indent=\"6.375in\" start-indent=\"-1in\">" ); 822 writeStartTag( BLOCK_TAG, "outdented.number.style" ); 823 824 if ( chapterNumber ) 825 { 826 writeStartTag( BLOCK_TAG, "chapter.title" ); 827 write( Integer.toString( chapter ) ); 828 writeEndTag( BLOCK_TAG ); 829 } 830 831 writeEndTag( BLOCK_TAG ); 832 writeEndTag( LIST_ITEM_LABEL_TAG ); 833 writeln( "<fo:list-item-body end-indent=\"1in\" start-indent=\"0in\">" ); 834 writeStartTag( BLOCK_TAG, "chapter.title" ); 835 836 if ( headerText == null ) 837 { 838 text( docTitle ); 839 } 840 else 841 { 842 text( headerText ); 843 } 844 845 writeEndTag( BLOCK_TAG ); 846 writeEndTag( LIST_ITEM_BODY_TAG ); 847 writeEndTag( LIST_ITEM_TAG ); 848 writeEndTag( LIST_BLOCK_TAG ); 849 writeEndTag( BLOCK_TAG ); 850 writeStartTag( BLOCK_TAG, "space-after.optimum", "0em" ); 851 writeEmptyTag( LEADER_TAG, "chapter.rule" ); 852 writeEndTag( BLOCK_TAG ); 853 } 854 855 /** 856 * Writes a table of contents. The DocumentModel has to contain a DocumentTOC for this to work. 857 */ 858 public void toc() 859 { 860 if ( docModel == null || docModel.getToc() == null || docModel.getToc().getItems() == null 861 || this.tocPosition == TOC_NONE ) 862 { 863 return; 864 } 865 866 DocumentTOC toc = docModel.getToc(); 867 868 writeln( "<fo:page-sequence master-reference=\"toc\" initial-page-number=\"1\" format=\"i\">" ); 869 regionBefore( toc.getName() ); 870 regionAfter( getFooterText() ); 871 writeStartTag( FLOW_TAG, "flow-name", "xsl-region-body" ); 872 writeStartTag( BLOCK_TAG, "id", "./toc" ); 873 chapterHeading( toc.getName(), false ); 874 writeln( "<fo:table table-layout=\"fixed\" width=\"100%\" >" ); 875 writeEmptyTag( TABLE_COLUMN_TAG, "column-width", "0.45in" ); 876 writeEmptyTag( TABLE_COLUMN_TAG, "column-width", "0.4in" ); 877 writeEmptyTag( TABLE_COLUMN_TAG, "column-width", "0.4in" ); 878 writeEmptyTag( TABLE_COLUMN_TAG, "column-width", "5in" ); // TODO {$maxBodyWidth - 1.25}in 879 writeStartTag( TABLE_BODY_TAG ); 880 881 writeTocItems( toc.getItems(), 1 ); 882 883 writeEndTag( TABLE_BODY_TAG ); 884 writeEndTag( TABLE_TAG ); 885 writeEndTag( BLOCK_TAG ); 886 writeEndTag( FLOW_TAG ); 887 writeEndTag( PAGE_SEQUENCE_TAG ); 888 } 889 890 private void writeTocItems( List<DocumentTOCItem> tocItems, int level ) 891 { 892 final int maxTocLevel = 4; 893 894 if ( level < 1 || level > maxTocLevel ) 895 { 896 return; 897 } 898 899 tocStack.push( new NumberedListItem( NUMBERING_DECIMAL ) ); 900 901 boolean printToc = ( level == 1 ); 902 903 for ( DocumentTOCItem tocItem : tocItems ) 904 { 905 String ref = getIdName( tocItem.getRef() ); 906 907 writeStartTag( TABLE_ROW_TAG, "keep-with-next", "auto" ); 908 909 if ( level > 2 ) 910 { 911 for ( int i = 0; i < level - 2; i++ ) 912 { 913 writeStartTag( TABLE_CELL_TAG ); 914 writeSimpleTag( BLOCK_TAG ); 915 writeEndTag( TABLE_CELL_TAG ); 916 } 917 } 918 919 writeStartTag( TABLE_CELL_TAG, "toc.cell" ); 920 writeStartTag( BLOCK_TAG, "toc.number.style" ); 921 922 NumberedListItem current = tocStack.peek(); 923 if ( printToc ) 924 { 925 // MPDF-59: no entry numbering for first, since it's the "Table of Contents" 926 printToc = false; 927 } 928 else 929 { 930 current.next(); 931 write( currentTocNumber() ); 932 } 933 934 writeEndTag( BLOCK_TAG ); 935 writeEndTag( TABLE_CELL_TAG ); 936 937 String span = "3"; 938 939 if ( level > 2 ) 940 { 941 span = Integer.toString( 5 - level ); 942 } 943 944 writeStartTag( TABLE_CELL_TAG, "number-columns-spanned", span, "toc.cell" ); 945 MutableAttributeSet atts = getFoConfiguration().getAttributeSet( "toc.h" + level + ".style" ); 946 atts.addAttribute( "text-align-last", "justify" ); 947 writeStartTag( BLOCK_TAG, atts ); 948 writeStartTag( BASIC_LINK_TAG, "internal-destination", ref ); 949 text( tocItem.getName() ); 950 writeEndTag( BASIC_LINK_TAG ); 951 writeEmptyTag( LEADER_TAG, "toc.leader.style" ); 952 writeStartTag( INLINE_TAG, "page.number" ); 953 writeEmptyTag( PAGE_NUMBER_CITATION_TAG, "ref-id", ref ); 954 writeEndTag( INLINE_TAG ); 955 writeEndTag( BLOCK_TAG ); 956 writeEndTag( TABLE_CELL_TAG ); 957 writeEndTag( TABLE_ROW_TAG ); 958 959 if ( tocItem.getItems() != null ) 960 { 961 writeTocItems( tocItem.getItems(), level + 1 ); 962 } 963 } 964 965 tocStack.pop(); 966 } 967 968 private String currentTocNumber() 969 { 970 StringBuilder ch = new StringBuilder( tocStack.get( 0 ).getListItemSymbol() ); 971 972 for ( int i = 1; i < tocStack.size(); i++ ) 973 { 974 ch.append( tocStack.get( i ).getListItemSymbol() ); 975 } 976 977 return ch.toString(); 978 } 979 980 /** 981 * Writes a fo:bookmark-tree. The DocumentModel has to contain a DocumentTOC for this to work. 982 */ 983 protected void pdfBookmarks() 984 { 985 if ( docModel == null || docModel.getToc() == null ) 986 { 987 return; 988 } 989 990 writeStartTag( BOOKMARK_TREE_TAG ); 991 992 renderBookmarkItems( docModel.getToc().getItems() ); 993 994 writeEndTag( BOOKMARK_TREE_TAG ); 995 } 996 997 private void renderBookmarkItems( List<DocumentTOCItem> items ) 998 { 999 for ( DocumentTOCItem tocItem : items ) 1000 { 1001 String ref = getIdName( tocItem.getRef() ); 1002 1003 writeStartTag( BOOKMARK_TAG, "internal-destination", ref ); 1004 writeStartTag( BOOKMARK_TITLE_TAG ); 1005 text( tocItem.getName() ); 1006 writeEndTag( BOOKMARK_TITLE_TAG ); 1007 1008 if ( tocItem.getItems() != null ) 1009 { 1010 renderBookmarkItems( tocItem.getItems() ); 1011 } 1012 1013 writeEndTag( BOOKMARK_TAG ); 1014 } 1015 } 1016 1017 /** 1018 * Writes a cover page. The DocumentModel has to contain a DocumentMeta for this to work. 1019 */ 1020 public void coverPage() 1021 { 1022 if ( this.docModel == null ) 1023 { 1024 return; 1025 } 1026 1027 DocumentCover cover = docModel.getCover(); 1028 DocumentMeta meta = docModel.getMeta(); 1029 1030 if ( cover == null && meta == null ) 1031 { 1032 return; // no information for cover page: ignore 1033 } 1034 1035 // TODO: remove hard-coded settings 1036 1037 writeStartTag( PAGE_SEQUENCE_TAG, "master-reference", "cover-page" ); 1038 writeStartTag( FLOW_TAG, "flow-name", "xsl-region-body" ); 1039 writeStartTag( BLOCK_TAG, "text-align", "center" ); 1040 writeln( "<fo:table table-layout=\"fixed\" width=\"100%\" >" ); 1041 writeEmptyTag( TABLE_COLUMN_TAG, "column-width", "3.125in" ); 1042 writeEmptyTag( TABLE_COLUMN_TAG, "column-width", "3.125in" ); 1043 writeStartTag( TABLE_BODY_TAG ); 1044 1045 writeCoverHead( cover ); 1046 writeCoverBody( cover, meta ); 1047 writeCoverFooter( cover, meta ); 1048 1049 writeEndTag( TABLE_BODY_TAG ); 1050 writeEndTag( TABLE_TAG ); 1051 writeEndTag( BLOCK_TAG ); 1052 writeEndTag( FLOW_TAG ); 1053 writeEndTag( PAGE_SEQUENCE_TAG ); 1054 } 1055 1056 private void writeCoverHead( DocumentCover cover ) 1057 { 1058 if ( cover == null ) 1059 { 1060 return; 1061 } 1062 1063 String compLogo = cover.getCompanyLogo(); 1064 String projLogo = cover.getProjectLogo(); 1065 1066 writeStartTag( TABLE_ROW_TAG, "height", COVER_HEADER_HEIGHT ); 1067 writeStartTag( TABLE_CELL_TAG ); 1068 1069 if ( StringUtils.isNotEmpty( compLogo ) ) 1070 { 1071 SinkEventAttributeSet atts = new SinkEventAttributeSet(); 1072 atts.addAttribute( "text-align", "left" ); 1073 atts.addAttribute( "vertical-align", "top" ); 1074 writeStartTag( BLOCK_TAG, atts ); 1075 figureGraphics( compLogo, getGraphicsAttributes( compLogo ) ); 1076 writeEndTag( BLOCK_TAG ); 1077 } 1078 1079 writeSimpleTag( BLOCK_TAG ); 1080 writeEndTag( TABLE_CELL_TAG ); 1081 writeStartTag( TABLE_CELL_TAG ); 1082 1083 if ( StringUtils.isNotEmpty( projLogo ) ) 1084 { 1085 SinkEventAttributeSet atts = new SinkEventAttributeSet(); 1086 atts.addAttribute( "text-align", "right" ); 1087 atts.addAttribute( "vertical-align", "top" ); 1088 writeStartTag( BLOCK_TAG, atts ); 1089 figureGraphics( projLogo, getGraphicsAttributes( projLogo ) ); 1090 writeEndTag( BLOCK_TAG ); 1091 } 1092 1093 writeSimpleTag( BLOCK_TAG ); 1094 writeEndTag( TABLE_CELL_TAG ); 1095 writeEndTag( TABLE_ROW_TAG ); 1096 } 1097 1098 private void writeCoverBody( DocumentCover cover, DocumentMeta meta ) 1099 { 1100 if ( cover == null && meta == null ) 1101 { 1102 return; 1103 } 1104 1105 String subtitle = null; 1106 String title; 1107 String type = null; 1108 String version = null; 1109 if ( cover == null ) 1110 { 1111 // aleady checked that meta != null 1112 getLog().debug( "The DocumentCover is not defined, using the DocumentMeta title as cover title." ); 1113 title = meta.getTitle(); 1114 } 1115 else 1116 { 1117 subtitle = cover.getCoverSubTitle(); 1118 title = cover.getCoverTitle(); 1119 type = cover.getCoverType(); 1120 version = cover.getCoverVersion(); 1121 } 1122 1123 writeln( "<fo:table-row keep-with-previous=\"always\" height=\"0.014in\">" ); 1124 writeStartTag( TABLE_CELL_TAG, "number-columns-spanned", "2" ); 1125 writeStartTag( BLOCK_TAG, "line-height", "0.014in" ); 1126 writeEmptyTag( LEADER_TAG, "chapter.rule" ); 1127 writeEndTag( BLOCK_TAG ); 1128 writeEndTag( TABLE_CELL_TAG ); 1129 writeEndTag( TABLE_ROW_TAG ); 1130 1131 writeStartTag( TABLE_ROW_TAG, "height", "7.447in" ); 1132 writeStartTag( TABLE_CELL_TAG, "number-columns-spanned", "2" ); 1133 writeln( "<fo:table table-layout=\"fixed\" width=\"100%\" >" ); 1134 writeEmptyTag( TABLE_COLUMN_TAG, "column-width", "2.083in" ); 1135 writeEmptyTag( TABLE_COLUMN_TAG, "column-width", "2.083in" ); 1136 writeEmptyTag( TABLE_COLUMN_TAG, "column-width", "2.083in" ); 1137 1138 writeStartTag( TABLE_BODY_TAG ); 1139 1140 writeStartTag( TABLE_ROW_TAG ); 1141 writeStartTag( TABLE_CELL_TAG, "number-columns-spanned", "3" ); 1142 writeSimpleTag( BLOCK_TAG ); 1143 writeEmptyTag( BLOCK_TAG, "space-before", "3.2235in" ); 1144 writeEndTag( TABLE_CELL_TAG ); 1145 writeEndTag( TABLE_ROW_TAG ); 1146 1147 writeStartTag( TABLE_ROW_TAG ); 1148 writeStartTag( TABLE_CELL_TAG ); 1149 writeEmptyTag( BLOCK_TAG, "space-after", "0.5in" ); 1150 writeEndTag( TABLE_CELL_TAG ); 1151 1152 writeStartTag( TABLE_CELL_TAG, "number-columns-spanned", "2", "cover.border.left" ); 1153 writeStartTag( BLOCK_TAG, "cover.title" ); 1154 text( title == null ? "" : title ); 1155 writeEndTag( BLOCK_TAG ); 1156 writeEndTag( TABLE_CELL_TAG ); 1157 writeEndTag( TABLE_ROW_TAG ); 1158 1159 writeStartTag( TABLE_ROW_TAG ); 1160 writeStartTag( TABLE_CELL_TAG ); 1161 writeSimpleTag( BLOCK_TAG ); 1162 writeEndTag( TABLE_CELL_TAG ); 1163 1164 writeStartTag( TABLE_CELL_TAG, "number-columns-spanned", "2", "cover.border.left.bottom" ); 1165 writeStartTag( BLOCK_TAG, "cover.subtitle" ); 1166 text( subtitle == null ? ( version == null ? "" : " v. " + version ) : subtitle ); 1167 writeEndTag( BLOCK_TAG ); 1168 writeStartTag( BLOCK_TAG, "cover.subtitle" ); 1169 text( type == null ? "" : type ); 1170 writeEndTag( BLOCK_TAG ); 1171 writeEndTag( TABLE_CELL_TAG ); 1172 writeEndTag( TABLE_ROW_TAG ); 1173 1174 writeEndTag( TABLE_BODY_TAG ); 1175 writeEndTag( TABLE_TAG ); 1176 1177 writeEndTag( TABLE_CELL_TAG ); 1178 writeEndTag( TABLE_ROW_TAG ); 1179 1180 writeStartTag( TABLE_ROW_TAG, "height", "0.014in" ); 1181 writeStartTag( TABLE_CELL_TAG, "number-columns-spanned", "2" ); 1182 writeln( "<fo:block space-after=\"0.2in\" line-height=\"0.014in\">" ); 1183 writeEmptyTag( LEADER_TAG, "chapter.rule" ); 1184 writeEndTag( BLOCK_TAG ); 1185 writeEndTag( TABLE_CELL_TAG ); 1186 writeEndTag( TABLE_ROW_TAG ); 1187 1188 writeStartTag( TABLE_ROW_TAG ); 1189 writeStartTag( TABLE_CELL_TAG, "number-columns-spanned", "2" ); 1190 writeSimpleTag( BLOCK_TAG ); 1191 writeEmptyTag( BLOCK_TAG, "space-before", "0.2in" ); 1192 writeEndTag( TABLE_CELL_TAG ); 1193 writeEndTag( TABLE_ROW_TAG ); 1194 } 1195 1196 private void writeCoverFooter( DocumentCover cover, DocumentMeta meta ) 1197 { 1198 if ( cover == null && meta == null ) 1199 { 1200 return; 1201 } 1202 1203 String date = null; 1204 String compName; 1205 if ( cover == null ) 1206 { 1207 // aleady checked that meta != null 1208 getLog().debug( "The DocumentCover is not defined, using the DocumentMeta author as company name." ); 1209 compName = meta.getAuthor(); 1210 } 1211 else 1212 { 1213 compName = cover.getCompanyName(); 1214 1215 if ( cover.getCoverdate() == null ) 1216 { 1217 cover.setCoverDate( new Date() ); 1218 date = cover.getCoverdate(); 1219 cover.setCoverDate( null ); 1220 } 1221 else 1222 { 1223 date = cover.getCoverdate(); 1224 } 1225 } 1226 1227 writeStartTag( TABLE_ROW_TAG, "height", "0.3in" ); 1228 1229 writeStartTag( TABLE_CELL_TAG ); 1230 MutableAttributeSet att = getFoConfiguration().getAttributeSet( "cover.subtitle" ); 1231 att.addAttribute( "height", "0.3in" ); 1232 att.addAttribute( "text-align", "left" ); 1233 writeStartTag( BLOCK_TAG, att ); 1234 text( compName == null ? ( cover.getAuthor() == null ? "" : cover.getAuthor() ) : compName ); 1235 writeEndTag( BLOCK_TAG ); 1236 writeEndTag( TABLE_CELL_TAG ); 1237 1238 writeStartTag( TABLE_CELL_TAG ); 1239 att = getFoConfiguration().getAttributeSet( "cover.subtitle" ); 1240 att.addAttribute( "height", "0.3in" ); 1241 att.addAttribute( "text-align", "right" ); 1242 writeStartTag( BLOCK_TAG, att ); 1243 text( date == null ? "" : date ); 1244 writeEndTag( BLOCK_TAG ); 1245 writeEndTag( TABLE_CELL_TAG ); 1246 1247 writeEndTag( TABLE_ROW_TAG ); 1248 } 1249 1250 private ResourceBundle getBundle( Locale locale ) 1251 { 1252 return ResourceBundle.getBundle( "doxia-fo", locale, this.getClass().getClassLoader() ); 1253 } 1254 1255 private SinkEventAttributeSet getGraphicsAttributes( String logo ) 1256 { 1257 MutableAttributeSet atts = null; 1258 1259 try 1260 { 1261 atts = DoxiaUtils.getImageAttributes( logo ); 1262 } 1263 catch ( IOException e ) 1264 { 1265 getLog().debug( e ); 1266 } 1267 1268 if ( atts == null ) 1269 { 1270 return new SinkEventAttributeSet( SinkEventAttributes.HEIGHT, COVER_HEADER_HEIGHT ); 1271 } 1272 1273 // FOP dpi: 72 1274 // Max width : 3.125 inch, table cell size, see #coverPage() 1275 final int maxWidth = 225; // 3.125 * 72 1276 1277 if ( Integer.parseInt( atts.getAttribute( SinkEventAttributes.WIDTH ).toString() ) > maxWidth ) 1278 { 1279 atts.addAttribute( "content-width", "3.125in" ); 1280 } 1281 1282 return new SinkEventAttributeSet( atts ); 1283 } 1284}