1 | // Copyright (C) 2008 Google Inc. |
2 | // |
3 | // Licensed under the Apache License, Version 2.0 (the "License"); |
4 | // you may not use this file except in compliance with the License. |
5 | // You may obtain a copy of the License at |
6 | // |
7 | // http://www.apache.org/licenses/LICENSE-2.0 |
8 | // |
9 | // Unless required by applicable law or agreed to in writing, software |
10 | // distributed under the License is distributed on an "AS IS" BASIS, |
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
12 | // See the License for the specific language governing permissions and |
13 | // limitations under the License. |
14 | |
15 | package com.google.caja.render; |
16 | |
17 | import com.google.caja.lexer.FilePosition; |
18 | import com.google.caja.lexer.InputSource; |
19 | import com.google.caja.lexer.TokenConsumer; |
20 | import com.google.caja.util.Join; |
21 | import com.google.caja.util.Pair; |
22 | |
23 | import java.util.ArrayList; |
24 | import java.util.Arrays; |
25 | import java.util.HashMap; |
26 | import java.util.List; |
27 | import java.util.Map; |
28 | import java.util.regex.Matcher; |
29 | import java.util.regex.Pattern; |
30 | |
31 | /** |
32 | * Renders rewritten source code interleaved with the original. E.g. |
33 | * {@code |
34 | * // Rewritten by cajoler. |
35 | * muckWith( IMPORTS___.muckWith( |
36 | * document.forms[0]) IMPORTS___.document.forms[0]); |
37 | * } |
38 | * |
39 | * @author mikesamuel@gmail.com |
40 | */ |
41 | public abstract class SideBySideRenderer implements TokenConsumer { |
42 | private final Map<InputSource, String[]> originalSourceLines; |
43 | private final Map<InputSource, Integer> maxLineSeen |
44 | = new HashMap<InputSource, Integer>(); |
45 | private final TokenConsumer renderer; |
46 | private FilePosition lastPos; |
47 | private FilePosition mark; |
48 | private FilePosition chunkStart; |
49 | /** Chunks of original source. */ |
50 | private final List<Chunk> chunks = new ArrayList<Chunk>(); |
51 | private StringBuilder renderedBuf; |
52 | |
53 | public SideBySideRenderer( |
54 | Map<InputSource, ? extends CharSequence> originalSource) { |
55 | this.originalSourceLines = new HashMap<InputSource, String[]>(); |
56 | for (Map.Entry<InputSource, ? extends CharSequence> e |
57 | : originalSource.entrySet()) { |
58 | this.originalSourceLines.put( |
59 | e.getKey(), e.getValue().toString().split("\r\n?|\n")); |
60 | } |
61 | this.renderedBuf = new StringBuilder(); |
62 | this.renderer = makeRenderer(this.renderedBuf); |
63 | } |
64 | |
65 | /** |
66 | * Called when rendered tokens have been processed for a line of original |
67 | * source. |
68 | * |
69 | * @param startOfLine a file position into the original source code. |
70 | * @param original zero or more lines of original source code. |
71 | * @param rendered one or more lines of rendered source code. |
72 | */ |
73 | protected abstract void emitLine( |
74 | FilePosition startOfLine, String original, String rendered); |
75 | |
76 | /** |
77 | * Called when we render a token from a different source than previously. |
78 | * This method does nothing, but may be overridden. |
79 | * @param previous the source from which the last rendered token came. |
80 | * @param next the source from which the next rendered token will come, |
81 | * unless switchSource is called again before {@link #consume}. |
82 | */ |
83 | protected void switchSource(InputSource previous, InputSource next) { |
84 | // noop |
85 | } |
86 | |
87 | protected abstract TokenConsumer makeRenderer(StringBuilder renderedSrc); |
88 | |
89 | public void mark(FilePosition pos) { |
90 | if (pos != null) { this.mark = pos; } |
91 | renderer.mark(pos); |
92 | } |
93 | |
94 | public void consume(String text) { |
95 | if (TokenClassification.isComment(text)) { return; } |
96 | if (!(mark != null |
97 | ? lastPos != null && mark.source().equals(lastPos.source()) |
98 | : lastPos == null)) { |
99 | emitLine(); |
100 | } else if (lastPos != null) { |
101 | if (mark.startLineNo() > lastPos.startLineNo() |
102 | && mark.startLineNo() >= lastLineNo(mark.source())) { |
103 | emitLine(); |
104 | } |
105 | } |
106 | |
107 | renderer.consume(text); |
108 | lastPos = mark; |
109 | } |
110 | |
111 | public void noMoreTokens() { |
112 | emitLine(); |
113 | renderer.noMoreTokens(); |
114 | |
115 | String renderedSrc = renderedBuf.toString(); |
116 | renderedBuf.setLength(0); |
117 | |
118 | InputSource lastSource = null; |
119 | for (Pair<String, Integer> chunk : splitChunks(renderedSrc)) { |
120 | String renderedChunk = chunk.a; |
121 | int chunkIndex = chunk.b; |
122 | Chunk originalChunk = ( |
123 | chunkIndex >= 0 ? chunks.get(chunkIndex) : new Chunk("", null)); |
124 | InputSource source = ( |
125 | originalChunk.start != null ? originalChunk.start.source() : null); |
126 | |
127 | if (!"".equals(renderedChunk) || !"".equals(originalChunk.src)) { |
128 | if (!(source != null |
129 | ? lastSource != null && source.equals(lastSource) |
130 | : lastSource == null)) { |
131 | switchSource(lastSource, source); |
132 | } |
133 | |
134 | emitLine(originalChunk.start, originalChunk.src, renderedChunk); |
135 | } |
136 | lastSource = source; |
137 | } |
138 | } |
139 | |
140 | private void emitLine() { |
141 | String originalSrc = ""; |
142 | if (chunkStart != null) { |
143 | int startLine = lastLineNo(chunkStart.source()) + 1; |
144 | int endLine = lastPos.endLineNo(); |
145 | if (lastPos.endCharInLine() == 1) { --endLine; } |
146 | originalSrc = originalSourceSnippet( |
147 | chunkStart.source(), startLine, endLine); |
148 | maxLineSeen.put(chunkStart.source(), endLine); |
149 | } |
150 | |
151 | int chunkId = chunks.size(); |
152 | chunks.add(new Chunk(originalSrc, chunkStart)); |
153 | renderer.consume("/*@" + chunkId + "*/"); |
154 | renderer.consume("\n"); |
155 | |
156 | chunkStart = mark; |
157 | } |
158 | |
159 | private static List<Pair<String, Integer>> splitChunks(String renderedSrc) { |
160 | Pattern p = Pattern.compile(" */\\*@([0-9]+)\\*/(?:\n|\r\n?|$)"); |
161 | Matcher m = p.matcher(renderedSrc); |
162 | int start = 0; |
163 | List<Pair<String, Integer>> chunks = new ArrayList<Pair<String, Integer>>(); |
164 | while (m.find()) { |
165 | int chunkIndex = Integer.parseInt(m.group(1)); |
166 | chunks.add( |
167 | Pair.pair(renderedSrc.substring(start, m.start()), chunkIndex)); |
168 | start = m.end(); |
169 | } |
170 | chunks.add(Pair.pair(renderedSrc.substring(start), -1)); |
171 | return chunks; |
172 | } |
173 | |
174 | private String originalSourceSnippet( |
175 | InputSource src, int startLine, int endLine) { |
176 | // FilePosition lines are 1-indexed, but arrays are zero-indexed. |
177 | startLine -= 1; |
178 | endLine -= 1; |
179 | |
180 | String[] lines = originalSourceLines.get(src); |
181 | if (lines == null || startLine >= lines.length) { return ""; } |
182 | endLine = Math.min(endLine, lines.length - 1); |
183 | |
184 | return Join.join( |
185 | "\n", Arrays.asList(lines).subList(startLine, endLine + 1)); |
186 | } |
187 | |
188 | private int lastLineNo(InputSource src) { |
189 | Integer ln = maxLineSeen.get(src); |
190 | return ln != null ? ln : 0; |
191 | } |
192 | |
193 | private static final class Chunk { |
194 | private final String src; |
195 | private final FilePosition start; |
196 | Chunk(String src, FilePosition start) { |
197 | this.start = start; |
198 | this.src = src; |
199 | } |
200 | } |
201 | } |