/* * Copyright (C) 2011 Laurent Caillette * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation, either * version 3 of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package org.novelang.rendering; import java.io.File; import java.io.OutputStream; import java.io.PrintWriter; import java.nio.charset.Charset; import java.util.Set; import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import static org.novelang.parser.NodeKind.*; import org.novelang.common.Location; import org.novelang.common.Nodepath; import org.novelang.common.Problem; import org.novelang.common.Renderable; import org.novelang.common.SyntacticTree; import org.novelang.common.TagBehavior; import org.novelang.common.metadata.MetadataHelper; import org.novelang.common.metadata.Page; import org.novelang.common.metadata.PageIdentifier; import org.novelang.parser.NodeKind; import org.novelang.parser.NodeKindTools; import org.novelang.rendering.multipage.PagesExtractor; /** * The only implementation of {@code Renderer} making sense as it delegates all specific * tasks to {@link org.novelang.rendering.FragmentWriter}. * * @author Laurent Caillette */ public class GenericRenderer implements Renderer { private final FragmentWriter fragmentWriter ; private final String whitespace ; private final boolean renderLocation ; private static final String DEFAULT_WHITESPACE = " " ; public GenericRenderer( final FragmentWriter fragmentWriter ) { this( fragmentWriter, false, DEFAULT_WHITESPACE ) ; } protected GenericRenderer( final FragmentWriter fragmentWriter, final boolean renderLocation, final String whitespace ) { this.fragmentWriter = Preconditions.checkNotNull( fragmentWriter ) ; this.whitespace = whitespace ; this.renderLocation = renderLocation ; } public GenericRenderer( final FragmentWriter fragmentWriter, final String defaultWhitespace ) { this( fragmentWriter, false, defaultWhitespace ) ; } public GenericRenderer( final FragmentWriter fragmentWriter, final boolean renderLocation ) { this( fragmentWriter, renderLocation, DEFAULT_WHITESPACE ) ; } @Override final public void render( final Renderable rendered, final OutputStream outputStream, final Page page, final File contentDirectory ) throws Exception { if( rendered.hasProblem() ) { renderProblems( rendered.getProblems(), outputStream ) ; } else { fragmentWriter.startWriting( outputStream, MetadataHelper.createMetadata( rendered.getRenderingCharset(), page, contentDirectory ) ) ; final SyntacticTree root = MetadataHelper .createMetadataDecoration( rendered.getDocumentTree(), page ) ; renderTreeInternal( root, null, null ) ; fragmentWriter.finishWriting() ; } } @Override public ImmutableMap< PageIdentifier, String > extractPages( final SyntacticTree documentTree ) throws Exception { if( fragmentWriter instanceof PagesExtractor ) { return ( ( PagesExtractor ) fragmentWriter ).extractPages( documentTree ) ; } else { return EMPTY_MAP ; } } @Override public RenditionMimeType getMimeType() { return fragmentWriter.getMimeType() ; } /** * TODO define a clearer contract forbidding nulls. * By now some parameters are null when rendering a multipage embedded stylesheet. * This works but gets things messy. * Maybe another method with no parameter could be OK. */ public void renderTree( final SyntacticTree tree, final OutputStream outputStream, final Charset renderingCharset, final Page page, final File contentDirectoryForResources ) throws Exception { fragmentWriter.startWriting( outputStream, MetadataHelper.createMetadata( renderingCharset, page, contentDirectoryForResources ) ) ; renderTreeInternal( MetadataHelper.createMetadataDecoration( tree, page ), null, null ) ; fragmentWriter.finishWriting() ; } private void renderTreeInternal( final SyntacticTree tree, final Nodepath kinship, final NodeKind previous ) throws Exception { final NodeKind nodeKind = NodeKindTools.ofRoot( tree ) ; final Nodepath newPath = ( createNodepath( kinship, nodeKind ) ) ; boolean isRootElement = false ; switch( nodeKind ) { case WORD_: final SyntacticTree wordTree = tree.getChildAt( 0 ) ; fragmentWriter.write( newPath, wordTree.getText() ) ; // Handle superscript if( tree.getChildCount() > 1 ) { for( int childIndex = 1 ; childIndex < tree.getChildCount() ; childIndex++ ) { final SyntacticTree child = tree.getChildAt( childIndex ) ; renderTreeInternal( child, newPath, WORD_ ) ; } } break ; case WORD_AFTER_CIRCUMFLEX_ACCENT: final SyntacticTree superscriptTree = tree.getChildAt( 0 ) ; fragmentWriter.start( newPath, isRootElement ) ; fragmentWriter.write( newPath, superscriptTree.getText() ) ; fragmentWriter.end( newPath ) ; break ; case BLOCK_OF_LITERAL_INSIDE_GRAVE_ACCENTS : case BLOCK_OF_LITERAL_INSIDE_GRAVE_ACCENT_PAIRS : writeLiteral( fragmentWriter, newPath, Spaces.normalizeLiteral( tree.getChildAt( 0 ).getText() ) ) ; break ; case ABSOLUTE_IDENTIFIER : case _EXPLICIT_IDENTIFIER : case _COLLIDING_EXPLICIT_IDENTIFIER : case _IMPLICIT_IDENTIFIER : fragmentWriter.start( newPath, isRootElement ) ; final StringBuilder builder = new StringBuilder() ; for( final SyntacticTree child : tree.getChildren() ) { builder.append( child.getText() ) ; } fragmentWriter.write( newPath, builder.toString() ) ; fragmentWriter.end( newPath ) ; break ; case URL_LITERAL : case TAG : case _IMPLICIT_TAG : case _EXPLICIT_TAG : case _PROMOTED_TAG : case _WORD_COUNT : case _PAGE_IDENTIFIER : case _PAGE_PATH : case _STYLE : case RAW_LINES : final SyntacticTree literalTree = tree.getChildAt( 0 ) ; writeLiteral( fragmentWriter, newPath, literalTree.getText() ) ; break ; case RESOURCE_LOCATION: case _IMAGE_WIDTH: case _IMAGE_HEIGHT: case APOSTROPHE_WORDMATE : case SIGN_COLON : case SIGN_COMMA : case SIGN_ELLIPSIS : case SIGN_EXCLAMATIONMARK : case SIGN_FULLSTOP : case SIGN_QUESTIONMARK : case SIGN_SEMICOLON : fragmentWriter.start( newPath, false ) ; fragmentWriter.write( newPath, tree.getChildAt( 0 ).getText() ) ; fragmentWriter.end( newPath ) ; break ; case LEVEL_INTRODUCER_INDENT_ : break ; // Is this still useful? EmbeddedListMangler should have discarded parsed tokens. case PARAGRAPH_AS_LIST_ITEM_WITH_TRIPLE_HYPHEN_ : case PARAGRAPH_AS_LIST_ITEM_WITH_DOUBLE_HYPHEN_AND_NUMBER_SIGN: processByDefault( tree, createNodepath( kinship, _PARAGRAPH_AS_LIST_ITEM ), false ) ; break ; // Is this still useful? EmbeddedListMangler should have discarded parsed tokens. case EMBEDDED_LIST_ITEM_WITH_HYPHEN_ : case EMBEDDED_LIST_ITEM_NUMBERED_ : processByDefault( tree, createNodepath( kinship, _EMBEDDED_LIST_ITEM ), false ) ; break ; case LEVEL_TITLE: processByDefault( tree, createNodepath( kinship, LEVEL_TITLE ), false ) ; break ; case OPUS: case NOVELLA: isRootElement = true ; default : processByDefault( tree, newPath, isRootElement ); break ; } } private static void writeLiteral( final FragmentWriter fragmentWriter, final Nodepath newPath, final String literal ) throws Exception { fragmentWriter.start( newPath, false ) ; fragmentWriter.writeLiteral( newPath, literal ) ; fragmentWriter.end( newPath ) ; } private Nodepath createNodepath( final Nodepath kinship, final NodeKind kind ) { return null == kinship ? new Nodepath( kind ) : new Nodepath( kinship, kind ); } private void processByDefault( final SyntacticTree tree, final Nodepath path, final boolean rootElement ) throws Exception { NodeKind previous; fragmentWriter.start( path, rootElement ) ; maybeWriteLocation( tree, path ) ; previous = null ; for( final SyntacticTree subtree : tree.getChildren() ) { final NodeKind subtreeNodeKind = NodeKindTools.ofRoot( subtree ); maybeWriteWhitespace( path, previous, subtreeNodeKind ) ; renderTreeInternal( subtree, path, previous ) ; previous = subtreeNodeKind; } fragmentWriter.end( path ) ; } private void maybeWriteWhitespace( final Nodepath path, final NodeKind previous, final NodeKind nodeKind ) throws Exception { if( ! hasBlockAfterTilde( path ) && Spaces.isTrigger( previous, nodeKind ) ) { fragmentWriter.write( path, whitespace ) ; } } private static final Set< TagBehavior > LOCATION_ENABLED_TAG_BEHAVIORS = ImmutableSet.of( TagBehavior.SCOPE, TagBehavior.TRAVERSABLE ) ; private void maybeWriteLocation( final SyntacticTree tree, final Nodepath path ) throws Exception { final Location location = tree.getLocation() ; final NodeKind nodeKind = path.getCurrent(); if( renderLocation && location != null && ( LOCATION_ENABLED_TAG_BEHAVIORS.contains( nodeKind.getTagBehavior() ) || NodeKind.PARAGRAPH_REGULAR == tree.getNodeKind() || NodeKind.LINES_OF_LITERAL == tree.getNodeKind() || NodeKind.CELL_ROWS_WITH_VERTICAL_LINE == tree.getNodeKind() ) ) { final Nodepath locationNodepath = new Nodepath( path, NodeKind._LOCATION ) ; fragmentWriter.start( locationNodepath, false ) ; fragmentWriter.write( locationNodepath, location.toHumanReadableForm() ) ; fragmentWriter.end( locationNodepath ) ; } } private static boolean hasBlockAfterTilde( final Nodepath path ) { if( path == null ) { return false ; } if( path.getCurrent() == BLOCK_AFTER_TILDE ) { return true ; } return hasBlockAfterTilde( path.getAncestor() ) ; } protected static void renderProblems( final Iterable< Problem > problems, final OutputStream outputStream ) { final PrintWriter writer = new PrintWriter( outputStream ) ; for( final Problem problem : problems ) { writer.println( problem.getLocation() ) ; writer.println( " " + problem.getMessage() ) ; } writer.flush() ; } }