001package gudusoft.gsqlparser.pp2.render; 002 003import gudusoft.gsqlparser.pp2.layout.LayoutContext; 004import gudusoft.gsqlparser.pp2.layout.LayoutDecisionView; 005import gudusoft.gsqlparser.pp2.token.Pp2Token; 006import gudusoft.gsqlparser.pp2.token.Pp2TokenStream; 007 008/** 009 * Renders a {@link LayoutContext}'s per-token {@link LayoutDecisionView}s to the 010 * final region string. 011 * 012 * <p>For each token it emits, in order: 013 * <ol> 014 * <li>{@code linebreaksBefore} newlines (decided value, else the token's 015 * original {@code precedingLinebreaks});</li> 016 * <li>{@code blanksBefore} spaces (decided value, else the token's original 017 * {@code precedingBlanks});</li> 018 * <li>the token text — the case-modified {@code textOverride} when decided, 019 * otherwise the source token text.</li> 020 * </ol> 021 * 022 * <p>The first token's leading whitespace is dropped: in production the 023 * inter-region trivia / preamble is owned by {@code RegionAssembler} (S15), and 024 * this writer emits only the region body. A plain {@link StringBuilder} is used 025 * rather than {@code pp.output.IPrinter} to avoid the {@code FormatterFactory} 026 * static {@code OutputConfig} field (plan forbidden #26). 027 * 028 * <p>Plan reference: §7.3/S31, §7.4/S31. 029 */ 030public final class Pp2OutputWriter { 031 032 /** 033 * Render the context's decisions to a string. 034 * 035 * @param context the layout context with decisions populated; must not be null 036 * @return the rendered region body; never null 037 * @throws NullPointerException if {@code context} is null 038 */ 039 public String write(LayoutContext context) { 040 if (context == null) throw new NullPointerException("context"); 041 Pp2TokenStream stream = context.getStream(); 042 StringBuilder sb = new StringBuilder(); 043 int n = stream.size(); 044 for (int i = 0; i < n; i++) { 045 Pp2Token t = stream.get(i); 046 LayoutDecisionView d = context.decisionAt(i); 047 int linebreaks = d.isLinebreaksDecided() 048 ? d.getLinebreaksBefore() : t.getPrecedingLinebreaks(); 049 int blanks = d.isBlanksDecided() 050 ? d.getBlanksBefore() : t.getPrecedingBlanks(); 051 // The region's first token leads the region body; its preceding 052 // whitespace is the assembler's preamble, not ours. 053 if (i == 0) { linebreaks = 0; blanks = 0; } 054 for (int k = 0; k < linebreaks; k++) sb.append('\n'); 055 for (int k = 0; k < blanks; k++) sb.append(' '); 056 sb.append(d.isTextDecided() ? d.getTextOverride() : t.getText()); 057 } 058 return sb.toString(); 059 } 060}