1/*
2 * Copyright (C) 2013 Apple Inc. All rights reserved.
3 *
4 * Redistribution and use in source and binary forms, with or without
5 * modification, are permitted provided that the following conditions
6 * are met:
7 * 1. Redistributions of source code must retain the above copyright
8 *    notice, this list of conditions and the following disclaimer.
9 * 2. Redistributions in binary form must reproduce the above copyright
10 *    notice, this list of conditions and the following disclaimer in the
11 *    documentation and/or other materials provided with the distribution.
12 *
13 * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
14 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
15 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
16 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
17 * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
18 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
19 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
20 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
21 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
22 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
23 * THE POSSIBILITY OF SUCH DAMAGE.
24 */
25
26function FormatterContentBuilder(mapping, originalLineEndings, formattedLineEndings, originalOffset, formattedOffset, indentString)
27{
28    this._originalContent = null;
29    this._formattedContent = [];
30    this._formattedContentLength = 0;
31
32    this._startOfLine = true;
33    this.lastTokenWasNewline = false;
34    this.lastTokenWasWhitespace = false;
35    this.lastNewlineAppendWasMultiple = false;
36
37    this._indent = 0;
38    this._indentString = indentString;
39    this._indentCache = ["", this._indentString];
40
41    this._mapping = mapping;
42    this._originalLineEndings = originalLineEndings || [];
43    this._formattedLineEndings = formattedLineEndings || [];
44    this._originalOffset = originalOffset || 0;
45    this._formattedOffset = formattedOffset || 0;
46
47    this._lastOriginalPosition = 0;
48    this._lastFormattedPosition = 0;
49}
50
51FormatterContentBuilder.prototype = {
52    constructor: FormatterContentBuilder,
53
54    // Public
55
56    get originalContent()
57    {
58        return this._originalContent;
59    },
60
61    get formattedContent()
62    {
63        var formatted = this._formattedContent.join("");
64        console.assert(formatted.length === this._formattedContentLength);
65        return formatted;
66    },
67
68    get mapping()
69    {
70        return this._mapping;
71    },
72
73    get originalLineEndings()
74    {
75        return this._originalLineEndings;
76    },
77
78    get formattedLineEndings()
79    {
80        return this._formattedLineEndings;
81    },
82
83    setOriginalContent: function(originalContent)
84    {
85        console.assert(!this._originalContent);
86        this._originalContent = originalContent;
87    },
88
89    appendToken: function(string, originalPosition)
90    {
91        if (this._startOfLine)
92            this._appendIndent();
93
94        this._addMappingIfNeeded(originalPosition);
95
96        this._append(string);
97        this._startOfLine = false;
98        this.lastTokenWasNewline = false;
99        this.lastTokenWasWhitespace = false;
100    },
101
102    appendSpace: function()
103    {
104        if (!this._startOfLine) {
105            this._append(" ");
106            this.lastTokenWasNewline = false;
107            this.lastTokenWasWhitespace = true;
108        }
109    },
110
111    appendNewline: function(force)
112    {
113        if ((!this.lastTokenWasNewline && !this._startOfLine) || force) {
114            this._append("\n");
115            this._addFormattedLineEnding();
116            this._startOfLine = true;
117            this.lastTokenWasNewline = true;
118            this.lastTokenWasWhitespace = false;
119            this.lastNewlineAppendWasMultiple = false;
120        }
121    },
122
123    appendMultipleNewlines: function(newlines)
124    {
125        console.assert(newlines > 0);
126
127        var wasMultiple = newlines > 1;
128
129        while (newlines-- > 0)
130            this.appendNewline(true);
131
132        if (wasMultiple)
133            this.lastNewlineAppendWasMultiple = true;
134    },
135
136    removeLastNewline: function()
137    {
138        console.assert(this.lastTokenWasNewline);
139        console.assert(this._formattedContent.lastValue === "\n");
140        if (this.lastTokenWasNewline) {
141            this._popNewLine();
142            this._startOfLine = false;
143            this.lastTokenWasNewline = false;
144            this.lastTokenWasWhitespace = false;
145        }
146    },
147
148    indent: function()
149    {
150        ++this._indent;
151    },
152
153    dedent: function()
154    {
155        --this._indent;
156
157        console.assert(this._indent >= 0);
158        if (this._indent < 0)
159            this._indent = 0;
160    },
161
162    addOriginalLineEnding: function(originalPosition)
163    {
164        this._originalLineEndings.push(originalPosition);
165    },
166
167    finish: function()
168    {
169        this.appendNewline();
170    },
171
172    // Private
173
174    _popNewLine: function()
175    {
176        var removed = this._formattedContent.pop();
177        this._formattedContentLength -= removed.length;
178        this._formattedLineEndings.pop();
179    },
180
181    _append: function(str)
182    {
183        this._formattedContent.push(str);
184        this._formattedContentLength += str.length;
185    },
186
187    _appendIndent: function()
188    {
189        // Indent is already in the cache.
190        if (this._indent < this._indentCache.length) {
191            this._append(this._indentCache[this._indent]);
192            return;
193        }
194
195        // Indent was not in the cache, fill up the cache up with what was needed.
196        const maxCacheIndent = 20;
197        var max = Math.min(this._indent, maxCacheIndent);
198        for (var i = this._indentCache.length; i <= max; ++i)
199            this._indentCache[i] = this._indentCache[i-1] + this._indentString;
200
201        // Append indents as needed.
202        var indent = this._indent;
203        do {
204            if (indent >= maxCacheIndent)
205                this._append(this._indentCache[maxCacheIndent]);
206            else
207                this._append(this._indentCache[indent]);
208            indent -= maxCacheIndent;
209        } while (indent > 0);
210    },
211
212    _addMappingIfNeeded: function(originalPosition)
213    {
214        if (originalPosition - this._lastOriginalPosition === this._formattedContentLength - this._lastFormattedPosition)
215            return;
216
217        this._mapping.original.push(this._originalOffset + originalPosition);
218        this._mapping.formatted.push(this._formattedOffset + this._formattedContentLength);
219
220        this._lastOriginalPosition = originalPosition;
221        this._lastFormattedPosition = this._formattedContentLength;
222    },
223
224    _addFormattedLineEnding: function()
225    {
226        console.assert(this._formattedContent.lastValue === "\n");
227        this._formattedLineEndings.push(this._formattedContentLength - 1);
228    }
229}
230