/* * [Reflower.java] * * Summary: handles low level reflow and indenting logic. * * Copyright: (c) 2010-2017 Roedy Green, Canadian Mind Products, http://mindprod.com * * Licence: This software may be copied and used freely for any purpose but military. * http://mindprod.com/contact/nonmil.html * * Requires: JDK 1.8+ * * Created with: JetBrains IntelliJ IDEA IDE http://www.jetbrains.com/idea/ * * Version History: * 1.0 2010-01-12 initial release. */ package com.mindprod.htmlreflow; import com.mindprod.common18.ST; import java.util.HashMap; /** * handles low level reflow and indenting logic. * * @author Roedy Green, Canadian Mind Products * @version 1.0 2010-01-12 initial release. * @since 2010-01-12 */ class Reflower { /** * lookup tag config given tag name */ private final HashMap tags; /** * \r\n \n \r string to separate lines in the output */ private final String lineSeparator; /** * target length for each reflowed line. */ private final int desiredLineLength; /** * if line gets longer than this, we split at attributes. */ private final int maxLineLength; /** * where we accumulate the reflowed text for the whole file being tidied */ private StringBuilder sb; /** * how many characters exist in the current line so for. */ private int col = 0; /** * current indent level. */ private int indent = 0; /** * how many NLs we have pending to add after previous line */ private int pendingNLs = 0; /** * Constructor * * @param desiredLineLength line length to reflow to. * @param maxLineLength lines will be split at attributes if they get longer than this. * @param lineSeparator Lf, CrLf or Cr, how lines should be ended. * @param tags array of tag configuration data. */ public Reflower( final int desiredLineLength, final int maxLineLength, final String lineSeparator, final TagConfig[] tags ) { this.desiredLineLength = desiredLineLength; this.maxLineLength = maxLineLength; this.lineSeparator = lineSeparator; this.tags = new HashMap<>( tags.length * 2 ); for ( TagConfig tag : tags ) { this.tags.put( tag.getTagName(), tag ); } } /** * append this fragment as a unit to the current line, or to the next line. * The fragment itself is may be split over two lines. * * @param fragment text to add to line. */ private void appendWithSplitting( final String fragment ) { // Should we split or not at all? if ( fragment.length() + col <= desiredLineLength ) { // don't need to split sb.append( fragment ); col += fragment.length(); } else { // find a blank in the fragment to split at. final int where = whereToSplit( fragment ); if ( where < 0 ) { appendWithoutSplitting( fragment ); } else { sb.append( ST.trimTrailing( fragment.substring( 0, where ) ) ); // we start a new line for second piece, splitting the fragment over two lines. sb.append( lineSeparator ); col = 0; padForIndent(); final String frag2 = ST.trimLeading( fragment.substring( where ) ); appendWithSplitting( frag2 ); // recursively, may generate several lines } } } /** * append this fragment as a unit to the current line, or to the next line. The fragment itself is not split * over two lines. * * @param fragment text to add to line. */ private void appendWithoutSplitting( final String fragment ) { // not splittable. Should we split before, after or not at all? if ( fragment.length() + col <= desiredLineLength || isLongerBetter( col, col + fragment.length() ) ) { // don't need to split sb.append( fragment ); col += fragment.length(); } else { // we start a new line for this piece sb.append( lineSeparator ); col = 0; padForIndent(); sb.append( fragment ); col += fragment.length(); } } /** * @param beforeTag how many NLs to insert before this fragment * @param tagIndent how much more to indent before displaying this fragment. -ve to reduce indent. * @param fragment a piece of text to add * @param splittable true if this fragment can be broken over two lines at a space. * @param afterTag hom many NLs to insert after this fragment. * @param nestIndent how much more to indent after displaying this fragment. -ve to reduce indent. */ void emit( int beforeTag, int tagIndent, String fragment, boolean splittable, int afterTag, int nestIndent ) { assert 0 <= beforeTag && beforeTag <= 2 : "beforeTag:" + beforeTag; assert 0 <= afterTag && afterTag <= 2 : "afterTag:" + afterTag; assert -8 <= tagIndent && tagIndent <= 8 : "tagIndent:" + tagIndent; assert -8 <= nestIndent && nestIndent <= 8 : "nestIndent:" + nestIndent; final int insertBeforeNLs = Math.max( beforeTag, pendingNLs ); for ( int i = 0; i < insertBeforeNLs; i++ ) { sb.append( lineSeparator ); col = 0; } indent += tagIndent; padForIndent(); if ( splittable ) { appendWithSplitting( fragment ); } else { appendWithoutSplitting( fragment ); } pendingNLs = afterTag; indent += nestIndent; } /** * is it better to make the line longer or shorter than the target length? * * @param shorter number of characters in the line for the shorter option. * @param longer number of characters in the line for the longer option. * * @return true if the longer option is preferable. */ boolean isLongerBetter( int shorter, int longer ) { int over = longer - desiredLineLength; int under = desiredLineLength - shorter; return over < under || shorter < 20; } /** * emit sufficient spaces to indent to current level */ private void padForIndent() { final int insertLeftPad = Math.max( indent - col, 0 ); for ( int i = 0; i < insertLeftPad; i++ ) { sb.append( ' ' ); } col += insertLeftPad; } /** * find the best space in the fragment to split on, * * @param fragment fragment to add. * * @return index of the best space to split on. -1 if none. */ int whereToSplit( String fragment ) { final int ideal = desiredLineLength - col; final int firstSpaceBeforeIdeal = fragment.lastIndexOf( ' ', ideal ); final int firstSpaceAfterIdeal = fragment.indexOf( ' ', ideal ); if ( firstSpaceBeforeIdeal >= 0 ) { if ( firstSpaceAfterIdeal >= 0 ) { if ( ideal - firstSpaceBeforeIdeal < firstSpaceAfterIdeal - ideal ) { return firstSpaceBeforeIdeal; } else { return firstSpaceAfterIdeal; } } else { // firstSpaceBeforeIdeal >= 0, firstSpaceAfterIdeal < 0 return firstSpaceBeforeIdeal; } } else { if ( firstSpaceAfterIdeal >= 0 ) { // firstSpaceBeforeIdeal < 0, firstSpaceAfterIdeal >= 0 return firstSpaceAfterIdeal; } else { // firstSpaceBeforeIdeal < 0, firstSpaceAfterIdeal < 0 return -1; // to indicate cannot find a good place to split. } } } /** * reflow the String contents of one file * * @param big the contents of the file * * @return the contents reflowed. */ public String reflowString( final String big ) { sb = new StringBuilder( big.length() * 150 / 100 ); return null; } /** * extract the completed reflowed document. */ public String toString() { // make sure exactly one NL on the end. if ( col != 0 ) { sb.append( lineSeparator ); } return sb.toString(); } }