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}