capacitor-dex-editor 0.0.38 → 0.0.39
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/android/src/main/AndroidManifest.xml +8 -0
- package/android/src/main/assets/availableSyntax.json +54 -0
- package/android/src/main/assets/c.json +42 -0
- package/android/src/main/assets/colors.json +21 -0
- package/android/src/main/assets/cpp.json +79 -0
- package/android/src/main/assets/dart.json +108 -0
- package/android/src/main/assets/java.json +46 -0
- package/android/src/main/assets/js.json +54 -0
- package/android/src/main/assets/json.json +33 -0
- package/android/src/main/assets/kotlin.json +53 -0
- package/android/src/main/assets/lua.json +54 -0
- package/android/src/main/assets/php.json +69 -0
- package/android/src/main/assets/python.json +87 -0
- package/android/src/main/assets/rust.json +139 -0
- package/android/src/main/assets/smali.json +131 -0
- package/android/src/main/assets/xml.json +96 -0
- package/android/src/main/java/com/aetherlink/dexeditor/DexEditorPluginPlugin.java +48 -0
- package/android/src/main/java/com/aetherlink/dexeditor/SmaliEditorActivity.java +205 -0
- package/android/src/main/java/com/aetherlink/dexeditor/editor/EditView.java +4022 -0
- package/android/src/main/java/com/aetherlink/dexeditor/editor/WordWrapLayout.java +275 -0
- package/android/src/main/java/com/aetherlink/dexeditor/editor/buffer/BufferCache.java +113 -0
- package/android/src/main/java/com/aetherlink/dexeditor/editor/buffer/GapBuffer.java +685 -0
- package/android/src/main/java/com/aetherlink/dexeditor/editor/component/ClipboardPanel.java +1380 -0
- package/android/src/main/java/com/aetherlink/dexeditor/editor/component/Magnifier.java +363 -0
- package/android/src/main/java/com/aetherlink/dexeditor/editor/highlight/Candidate.java +52 -0
- package/android/src/main/java/com/aetherlink/dexeditor/editor/highlight/CommentDef.java +47 -0
- package/android/src/main/java/com/aetherlink/dexeditor/editor/highlight/LineResult.java +49 -0
- package/android/src/main/java/com/aetherlink/dexeditor/editor/highlight/MHSyntaxHighlightEngine.java +841 -0
- package/android/src/main/java/com/aetherlink/dexeditor/editor/highlight/Rule.java +53 -0
- package/android/src/main/java/com/aetherlink/dexeditor/editor/highlight/Token.java +48 -0
- package/android/src/main/java/com/aetherlink/dexeditor/editor/listener/OnTextChangedListener.java +6 -0
- package/android/src/main/java/com/aetherlink/dexeditor/editor/utils/Pair.java +80 -0
- package/android/src/main/java/com/aetherlink/dexeditor/editor/utils/ScreenUtils.java +63 -0
- package/android/src/main/res/drawable/abc_text_cursor_material.xml +9 -0
- package/android/src/main/res/drawable/abc_text_select_handle_left_mtrl.png +0 -0
- package/android/src/main/res/drawable/abc_text_select_handle_middle_mtrl.png +0 -0
- package/android/src/main/res/drawable/abc_text_select_handle_right_mtrl.png +0 -0
- package/android/src/main/res/drawable/ic_arrow_back.xml +12 -0
- package/android/src/main/res/drawable/ic_copy.xml +11 -0
- package/android/src/main/res/drawable/ic_cut.xml +10 -0
- package/android/src/main/res/drawable/ic_delete.xml +12 -0
- package/android/src/main/res/drawable/ic_edit_white_24dp.xml +10 -0
- package/android/src/main/res/drawable/ic_goto.xml +10 -0
- package/android/src/main/res/drawable/ic_launcher_background.xml +186 -0
- package/android/src/main/res/drawable/ic_look_white_24dp.xml +11 -0
- package/android/src/main/res/drawable/ic_more.xml +10 -0
- package/android/src/main/res/drawable/ic_open_link.xml +11 -0
- package/android/src/main/res/drawable/ic_paste.xml +11 -0
- package/android/src/main/res/drawable/ic_redo_white_24dp.xml +10 -0
- package/android/src/main/res/drawable/ic_select.xml +11 -0
- package/android/src/main/res/drawable/ic_select_all.xml +10 -0
- package/android/src/main/res/drawable/ic_share.xml +11 -0
- package/android/src/main/res/drawable/ic_toggle_comment.xml +12 -0
- package/android/src/main/res/drawable/ic_translate.xml +10 -0
- package/android/src/main/res/drawable/ic_undo_white_24dp.xml +11 -0
- package/android/src/main/res/drawable/magnifier_bg.xml +5 -0
- package/android/src/main/res/drawable/popup_background.xml +8 -0
- package/android/src/main/res/drawable/ripple_effect.xml +9 -0
- package/android/src/main/res/drawable/selection_menu_background.xml +5 -0
- package/android/src/main/res/layout/custom_selection_menu.xml +44 -0
- package/android/src/main/res/layout/expand_button.xml +23 -0
- package/android/src/main/res/layout/item_autocomplete.xml +25 -0
- package/android/src/main/res/layout/magnifier_popup.xml +17 -0
- package/android/src/main/res/layout/menu_item.xml +30 -0
- package/android/src/main/res/layout/text_selection_menu.xml +36 -0
- package/package.json +1 -1
package/android/src/main/java/com/aetherlink/dexeditor/editor/highlight/MHSyntaxHighlightEngine.java
ADDED
|
@@ -0,0 +1,841 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* MH-TextEditor - An Advanced and optimized TextEditor for android
|
|
3
|
+
* Copyright 2025, developer-krushna
|
|
4
|
+
*
|
|
5
|
+
* Redistribution and use in source and binary forms, with or without
|
|
6
|
+
* modification, are permitted provided that the following conditions are
|
|
7
|
+
* met:
|
|
8
|
+
*
|
|
9
|
+
* * Redistributions of source code must retain the above copyright
|
|
10
|
+
* notice, this list of conditions and the following disclaimer.
|
|
11
|
+
* * Redistributions in binary form must reproduce the above
|
|
12
|
+
* copyright notice, this list of conditions and the following disclaimer
|
|
13
|
+
* in the documentation and/or other materials provided with the
|
|
14
|
+
* distribution.
|
|
15
|
+
* * Neither the name of developer-krushna nor the names of its
|
|
16
|
+
* contributors may be used to endorse or promote products derived from
|
|
17
|
+
* this software without specific prior written permission.
|
|
18
|
+
*
|
|
19
|
+
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
|
20
|
+
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
|
21
|
+
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
|
22
|
+
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
|
23
|
+
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
|
24
|
+
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
|
25
|
+
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
|
26
|
+
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
|
27
|
+
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|
28
|
+
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
29
|
+
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
* Please contact Krushna by email modder-hub@zohomail.in if you need
|
|
33
|
+
* additional information or have any questions
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
package com.aetherlink.dexeditor.editor.highlight;
|
|
37
|
+
|
|
38
|
+
import android.content.Context;
|
|
39
|
+
import android.graphics.Canvas;
|
|
40
|
+
import android.graphics.Color;
|
|
41
|
+
import android.graphics.Paint;
|
|
42
|
+
import android.text.Layout;
|
|
43
|
+
import android.text.SpannableString;
|
|
44
|
+
import android.text.Spanned;
|
|
45
|
+
import android.text.StaticLayout;
|
|
46
|
+
import android.text.TextDirectionHeuristics;
|
|
47
|
+
import android.text.TextPaint;
|
|
48
|
+
import android.text.style.ForegroundColorSpan;
|
|
49
|
+
import android.util.Log;
|
|
50
|
+
import java.io.InputStream;
|
|
51
|
+
import java.nio.charset.StandardCharsets;
|
|
52
|
+
import java.util.ArrayList;
|
|
53
|
+
import java.util.Arrays;
|
|
54
|
+
import java.util.Collections;
|
|
55
|
+
import java.util.Comparator;
|
|
56
|
+
import java.util.HashMap;
|
|
57
|
+
import java.util.HashSet;
|
|
58
|
+
import java.util.Iterator;
|
|
59
|
+
import java.util.LinkedHashMap;
|
|
60
|
+
import java.util.List;
|
|
61
|
+
import java.util.Map;
|
|
62
|
+
import java.util.Set;
|
|
63
|
+
import java.util.regex.Matcher;
|
|
64
|
+
import java.util.regex.Pattern;
|
|
65
|
+
import org.json.JSONArray;
|
|
66
|
+
import org.json.JSONObject;
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Author : Krushna Chandra Maharna(@developer-krushna)
|
|
70
|
+
*
|
|
71
|
+
* <p>This is a regex based Syntax highlighter .. Which may cause severe CPU usage during startup
|
|
72
|
+
* time but overall ok for highlighting code.
|
|
73
|
+
*
|
|
74
|
+
* <p>There is no feature for commenting multi line based (Which currently iam learning)
|
|
75
|
+
*
|
|
76
|
+
* <p>Some incorrect highlight may occur.
|
|
77
|
+
*/
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* MHSyntaxHighlightEngine ------------------------ A syntax highlighting engine that parses and
|
|
81
|
+
* colors text using regex-based rules.
|
|
82
|
+
*
|
|
83
|
+
* <p>It supports multiple languages (via JSON rule files) and color themes (day/night). The engine
|
|
84
|
+
* can draw highlighted text line-by-line on a Canvas, caching results for speed.
|
|
85
|
+
*/
|
|
86
|
+
public class MHSyntaxHighlightEngine {
|
|
87
|
+
|
|
88
|
+
private static final String TAG = "MHSyntaxHighlightEngine";
|
|
89
|
+
|
|
90
|
+
// Mapping of style names → colors (loaded from colors.json)
|
|
91
|
+
private final Map<String, Integer> colors = new HashMap<String, Integer>();
|
|
92
|
+
|
|
93
|
+
// All syntax rules loaded from the language JSON file
|
|
94
|
+
private final List<Rule> rules = new ArrayList<Rule>();
|
|
95
|
+
|
|
96
|
+
// Paint object used for text rendering
|
|
97
|
+
private final TextPaint paint;
|
|
98
|
+
|
|
99
|
+
// True if using dark mode (night colors)
|
|
100
|
+
private final boolean darkMode;
|
|
101
|
+
|
|
102
|
+
// save comment block
|
|
103
|
+
private final List<CommentDef> commentDefs = new ArrayList<>();
|
|
104
|
+
|
|
105
|
+
// preserve comment block
|
|
106
|
+
public String commentBlock;
|
|
107
|
+
|
|
108
|
+
private static final Set<
|
|
109
|
+
String> VALID_ESCAPES = new HashSet<>(Arrays.asList("n", "t", "r", "b", "f", "\\", "'", "\"", "u"));
|
|
110
|
+
// persistent multi-line block comment state (per SyntaxConfig instance)
|
|
111
|
+
|
|
112
|
+
private final Paint bgPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* LRU cache for per-line tokenized data. Key: line index Value: list of tokens for that line
|
|
116
|
+
*/
|
|
117
|
+
private final LinkedHashMap<Integer, LineResult> lineCache = new LinkedHashMap<
|
|
118
|
+
Integer, LineResult>(512, 0.75f, true) {
|
|
119
|
+
private static final int MAX = 1000; // Maximum cached lines
|
|
120
|
+
|
|
121
|
+
@Override
|
|
122
|
+
protected boolean removeEldestEntry(Map.Entry<Integer, LineResult> eldest) {
|
|
123
|
+
return size() > MAX;
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
/** Constructor: Initializes the engine with color and language configurations. */
|
|
128
|
+
public MHSyntaxHighlightEngine(Context ctx, TextPaint textPaint, String languageAssetFile, boolean darkMode) {
|
|
129
|
+
this.paint = textPaint;
|
|
130
|
+
this.darkMode = darkMode;
|
|
131
|
+
try {
|
|
132
|
+
initColors(ctx);
|
|
133
|
+
initLanguage(ctx, languageAssetFile);
|
|
134
|
+
} catch (Exception ex) {
|
|
135
|
+
ex.printStackTrace();
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** Loads color definitions from assets/colors.json */
|
|
140
|
+
private void initColors(Context ctx) throws Exception {
|
|
141
|
+
String s = loadAsset(ctx, "colors.json");
|
|
142
|
+
JSONObject jo = new JSONObject(s);
|
|
143
|
+
Iterator<String> it = jo.keys();
|
|
144
|
+
while (it.hasNext()) {
|
|
145
|
+
String k = it.next();
|
|
146
|
+
JSONObject col = jo.getJSONObject(k);
|
|
147
|
+
// Choose "day" or "night" color based on darkMode flag
|
|
148
|
+
String hex = darkMode ? col.getString("night") : col.getString("day");
|
|
149
|
+
colors.put(k, Color.parseColor(hex));
|
|
150
|
+
}
|
|
151
|
+
// Ensure default color exists
|
|
152
|
+
if (!colors.containsKey("default")) colors.put("default", Color.BLACK);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** Loads language-specific highlighting rules from assets/langFile (JSON) */
|
|
156
|
+
private void initLanguage(Context ctx, String langFile) throws Exception {
|
|
157
|
+
String s = loadAsset(ctx, langFile);
|
|
158
|
+
JSONObject lang = new JSONObject(s);
|
|
159
|
+
|
|
160
|
+
loadCommentDefsFromLang(lang);
|
|
161
|
+
|
|
162
|
+
// Predefined regex snippets used in rules
|
|
163
|
+
Map<String, String> defines = new HashMap<String, String>();
|
|
164
|
+
if (lang.has("defines")) {
|
|
165
|
+
JSONObject def = lang.getJSONObject("defines");
|
|
166
|
+
Iterator<String> dik = def.keys();
|
|
167
|
+
while (dik.hasNext()) {
|
|
168
|
+
String dn = dik.next();
|
|
169
|
+
Object dv = def.get(dn);
|
|
170
|
+
if (dv instanceof String) {
|
|
171
|
+
defines.put(dn, (String) dv);
|
|
172
|
+
} else if (dv instanceof JSONObject) {
|
|
173
|
+
JSONObject dobj = (JSONObject) dv;
|
|
174
|
+
if (dobj.has("regex")) defines.put(dn, dobj.getString("regex"));
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (!lang.has("rules")) return;
|
|
180
|
+
JSONArray arr = lang.getJSONArray("rules");
|
|
181
|
+
|
|
182
|
+
// Parse each rule
|
|
183
|
+
for (int i = 0; i < arr.length(); i++) {
|
|
184
|
+
JSONObject rj = arr.getJSONObject(i);
|
|
185
|
+
|
|
186
|
+
// Handle "include" rules that reference defines
|
|
187
|
+
if (rj.has("include")) {
|
|
188
|
+
String inc = rj.getString("include");
|
|
189
|
+
if (defines.containsKey(inc)) {
|
|
190
|
+
JSONObject nr = new JSONObject();
|
|
191
|
+
nr.put("regex", defines.get(inc));
|
|
192
|
+
nr.put("type", rj.optString("type", "default"));
|
|
193
|
+
// preserve lineBackground if present in original rule
|
|
194
|
+
if (rj.has("lineBackground"))
|
|
195
|
+
nr.put("lineBackground", rj.getString("lineBackground"));
|
|
196
|
+
rj = nr;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Handle keyword arrays → combined regex
|
|
201
|
+
if (rj.has("keywords")) {
|
|
202
|
+
JSONArray kw = rj.getJSONArray("keywords");
|
|
203
|
+
ArrayList<String> list = new ArrayList<String>();
|
|
204
|
+
for (int k = 0; k < kw.length(); k++) list.add(kw.getString(k));
|
|
205
|
+
// Sort keywords longest-first to avoid partial matches
|
|
206
|
+
Collections.sort(list, new Comparator<String>() {
|
|
207
|
+
public int compare(String a, String b) {
|
|
208
|
+
return b.length() - a.length();
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
// Build regex for keywords
|
|
212
|
+
StringBuilder sb = new StringBuilder();
|
|
213
|
+
for (int k = 0; k < list.size(); k++) {
|
|
214
|
+
if (k > 0) sb.append("|");
|
|
215
|
+
sb.append(Pattern.quote(list.get(k)));
|
|
216
|
+
}
|
|
217
|
+
// Pattern ensures keywords are bounded by whitespace or brackets
|
|
218
|
+
String patternStr = "(?:(?<=^)|(?<=\\s)|(?<=\\())(?:(?:" + sb.toString() + "))(?![A-Za-z0-9_/$\\.])";
|
|
219
|
+
Rule r = new Rule();
|
|
220
|
+
r.type = rj.optString("type", "keyword");
|
|
221
|
+
r.pattern = Pattern.compile(patternStr, Pattern.MULTILINE);
|
|
222
|
+
r.groupStyles = null;
|
|
223
|
+
r.priority = i;
|
|
224
|
+
// read optional lineBackground
|
|
225
|
+
if (rj.has("lineBackground")) {
|
|
226
|
+
r.lineBackground = rj.optString("lineBackground", null);
|
|
227
|
+
if (r.lineBackground != null && !r.lineBackground.isEmpty()) {
|
|
228
|
+
try {
|
|
229
|
+
r.lineBackgroundColor = Color.parseColor(r.lineBackground);
|
|
230
|
+
} catch (Exception ex) {
|
|
231
|
+
r.lineBackgroundColor = null;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
rules.add(r);
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Regular regex-based rules
|
|
240
|
+
if (!rj.has("regex")) continue;
|
|
241
|
+
Rule r = new Rule();
|
|
242
|
+
r.type = rj.optString("type", null);
|
|
243
|
+
r.pattern = Pattern.compile(rj.getString("regex"), Pattern.MULTILINE);
|
|
244
|
+
r.priority = i;
|
|
245
|
+
|
|
246
|
+
// Optional group-specific styles
|
|
247
|
+
if (rj.has("groupStyles")) {
|
|
248
|
+
r.groupStyles = new HashMap<Integer, String>();
|
|
249
|
+
JSONObject gs = rj.getJSONObject("groupStyles");
|
|
250
|
+
Iterator<String> gk = gs.keys();
|
|
251
|
+
while (gk.hasNext()) {
|
|
252
|
+
String key = gk.next();
|
|
253
|
+
try {
|
|
254
|
+
int gi = Integer.parseInt(key);
|
|
255
|
+
r.groupStyles.put(gi, gs.getString(key));
|
|
256
|
+
} catch (Exception ignore) {
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
} else {
|
|
260
|
+
r.groupStyles = null;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// NEW: optional line background on a rule (hex string)
|
|
264
|
+
if (rj.has("lineBackground")) {
|
|
265
|
+
r.lineBackground = rj.optString("lineBackground", null);
|
|
266
|
+
if (r.lineBackground != null && !r.lineBackground.isEmpty()) {
|
|
267
|
+
try {
|
|
268
|
+
r.lineBackgroundColor = Color.parseColor(r.lineBackground);
|
|
269
|
+
} catch (Exception ex) {
|
|
270
|
+
r.lineBackgroundColor = null;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
rules.add(r);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Loading comment object from the langauage file
|
|
280
|
+
private void loadCommentDefsFromLang(JSONObject lang) {
|
|
281
|
+
commentDefs.clear();
|
|
282
|
+
commentBlock = null;
|
|
283
|
+
try {
|
|
284
|
+
if (!lang.has("comment")) return;
|
|
285
|
+
Object c = lang.get("comment");
|
|
286
|
+
if (c instanceof JSONObject) {
|
|
287
|
+
JSONObject o = (JSONObject) c;
|
|
288
|
+
String s = o.optString("startsWith", null);
|
|
289
|
+
String e = o.optString("endsWith", null);
|
|
290
|
+
if (s != null && !s.isEmpty() && (e == null || e.isEmpty())) {
|
|
291
|
+
commentBlock = s;
|
|
292
|
+
}
|
|
293
|
+
if (s != null && !s.isEmpty()) {
|
|
294
|
+
commentDefs.add(new CommentDef(s, e));
|
|
295
|
+
}
|
|
296
|
+
} else if (c instanceof JSONArray) {
|
|
297
|
+
JSONArray arr = (JSONArray) c;
|
|
298
|
+
for (int i = 0; i < arr.length(); i++) {
|
|
299
|
+
JSONObject o = arr.optJSONObject(i);
|
|
300
|
+
if (o == null) continue;
|
|
301
|
+
String s = o.optString("startsWith", null);
|
|
302
|
+
String e = o.optString("endsWith", null);
|
|
303
|
+
if (s != null && !s.isEmpty() && (e == null || e.isEmpty())) {
|
|
304
|
+
commentBlock = s;
|
|
305
|
+
}
|
|
306
|
+
if (s != null && !s.isEmpty()) {
|
|
307
|
+
commentDefs.add(new CommentDef(s, e));
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
} catch (Exception ex) {
|
|
312
|
+
// load fail instead of crash
|
|
313
|
+
Log.w(TAG, "Failed to load comment defs", ex);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// Helper method, usefull for EditView for extracting comment block
|
|
318
|
+
public String getCommentSyntaxBlock() {
|
|
319
|
+
return commentBlock;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/** Reads a file from the assets directory as UTF-8 text. */
|
|
323
|
+
private String loadAsset(Context ctx, String name) throws Exception {
|
|
324
|
+
InputStream is = ctx.getAssets().open(name);
|
|
325
|
+
byte[] b = new byte[is.available()];
|
|
326
|
+
is.read(b);
|
|
327
|
+
is.close();
|
|
328
|
+
return new String(b, StandardCharsets.UTF_8);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/** Clears the entire token cache */
|
|
332
|
+
public void clearCache() {
|
|
333
|
+
synchronized (lineCache) {
|
|
334
|
+
lineCache.clear();
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// Special case for loading line background color
|
|
339
|
+
public void drawLineBackground(Canvas canvas,
|
|
340
|
+
String line,
|
|
341
|
+
int index,
|
|
342
|
+
int left,
|
|
343
|
+
int top,
|
|
344
|
+
int right,
|
|
345
|
+
int bottom) {
|
|
346
|
+
LineResult result = getOrTokenize(index, line);
|
|
347
|
+
|
|
348
|
+
if (result.backgroundColor != null) {
|
|
349
|
+
bgPaint.setColor(result.backgroundColor);
|
|
350
|
+
canvas.drawRect(left, top, right, bottom, bgPaint);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/** Draws a single line of highlighted text on the canvas. */
|
|
355
|
+
public void drawLineText(Canvas canvas,
|
|
356
|
+
String line,
|
|
357
|
+
int index,
|
|
358
|
+
int x,
|
|
359
|
+
int y) {
|
|
360
|
+
LineResult result = getOrTokenize(index, line);
|
|
361
|
+
|
|
362
|
+
// Only tokens → no background here
|
|
363
|
+
renderTokens(canvas, line, result.tokens, x, y);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/** Optimized shared cache lookup */
|
|
367
|
+
private LineResult getOrTokenize(int index, String line) {
|
|
368
|
+
LineResult result;
|
|
369
|
+
|
|
370
|
+
synchronized (lineCache) {
|
|
371
|
+
result = lineCache.get(index);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
if (result != null) return result;
|
|
375
|
+
|
|
376
|
+
// Tokenize
|
|
377
|
+
result = tokenizeLine(line);
|
|
378
|
+
|
|
379
|
+
synchronized (lineCache) {
|
|
380
|
+
lineCache.put(index, result);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
return result;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* Tokenizes a line into styled segments according to the rules. Resolves overlaps and converts
|
|
388
|
+
* candidates into tokens.
|
|
389
|
+
*/
|
|
390
|
+
private LineResult tokenizeLine(String line) {
|
|
391
|
+
ArrayList<Candidate> all = new ArrayList<Candidate>();
|
|
392
|
+
int L = (line == null) ? 0 : line.length();
|
|
393
|
+
if (L == 0) return new LineResult(new ArrayList<Token>(), null);
|
|
394
|
+
|
|
395
|
+
// pre: simple-scanner found big ranges (strings, comments)
|
|
396
|
+
ArrayList<Candidate> pre = new ArrayList<Candidate>();
|
|
397
|
+
|
|
398
|
+
// overrides: small spans inside strings (valid escapes -> "number", invalid -> "error")
|
|
399
|
+
ArrayList<Candidate> overrides = new ArrayList<Candidate>();
|
|
400
|
+
|
|
401
|
+
int i = 0;
|
|
402
|
+
while (i < L) {
|
|
403
|
+
char ch = line.charAt(i);
|
|
404
|
+
|
|
405
|
+
// QUOTE: " or '
|
|
406
|
+
if (ch == '"' || ch == '\'') {
|
|
407
|
+
char quote = ch;
|
|
408
|
+
int start = i;
|
|
409
|
+
i++; // move past opening quote
|
|
410
|
+
boolean escaped = false;
|
|
411
|
+
while (i < L) {
|
|
412
|
+
char c2 = line.charAt(i);
|
|
413
|
+
if (c2 == '\\' && !escaped) {
|
|
414
|
+
escaped = true;
|
|
415
|
+
i++;
|
|
416
|
+
continue;
|
|
417
|
+
}
|
|
418
|
+
if (c2 == quote && !escaped) {
|
|
419
|
+
i++; // include closing quote
|
|
420
|
+
break;
|
|
421
|
+
}
|
|
422
|
+
escaped = false;
|
|
423
|
+
i++;
|
|
424
|
+
}
|
|
425
|
+
int end = i; // exclusive
|
|
426
|
+
if (end > start) {
|
|
427
|
+
// add the full string span as a high-priority candidate (keeps string color)
|
|
428
|
+
pre.add(new Candidate(start, end, "string", -1000));
|
|
429
|
+
|
|
430
|
+
// process escapes inside string, but add them to overrides (so they don't
|
|
431
|
+
// prevent the string span)
|
|
432
|
+
int p = start + 1; // skip opening quote
|
|
433
|
+
while (p < end - 1) { // need at least "\" + next char
|
|
434
|
+
if (line.charAt(p) != '\\') {
|
|
435
|
+
p++;
|
|
436
|
+
continue;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// count consecutive backslashes starting at p
|
|
440
|
+
int bsStart = p;
|
|
441
|
+
int count = 0;
|
|
442
|
+
while (p < end && line.charAt(p) == '\\') {
|
|
443
|
+
count++;
|
|
444
|
+
p++;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// if the run reaches the end of string
|
|
448
|
+
if (p >= end) {
|
|
449
|
+
if ((count % 2) == 1) {
|
|
450
|
+
// dangling backslash -> mark the last backslash as error
|
|
451
|
+
int lastSlash = bsStart + count - 1;
|
|
452
|
+
overrides.add(new Candidate(lastSlash, lastSlash + 1, "error", -2000));
|
|
453
|
+
}
|
|
454
|
+
break;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
char next = line.charAt(p); // char after run
|
|
458
|
+
|
|
459
|
+
if ((count % 2) == 1) { // odd -> last backslash introduces escape
|
|
460
|
+
int lastSlashIndex = bsStart + count - 1;
|
|
461
|
+
|
|
462
|
+
if (next == 'u') {
|
|
463
|
+
int hexStart = p + 1;
|
|
464
|
+
int hexEnd = hexStart + 4;
|
|
465
|
+
boolean validUnicode = true;
|
|
466
|
+
if (hexEnd <= end) {
|
|
467
|
+
for (int h = hexStart; h < hexEnd; h++) {
|
|
468
|
+
char hx = line.charAt(h);
|
|
469
|
+
boolean isHex = (hx >= '0' && hx <= '9')
|
|
470
|
+
|| (hx >= 'a' && hx <= 'f')
|
|
471
|
+
|| (hx >= 'A' && hx <= 'F');
|
|
472
|
+
if (!isHex) {
|
|
473
|
+
validUnicode = false;
|
|
474
|
+
break;
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
} else validUnicode = false;
|
|
478
|
+
|
|
479
|
+
if (validUnicode) {
|
|
480
|
+
int tokenEnd = hexEnd;
|
|
481
|
+
overrides.add(new Candidate(lastSlashIndex, tokenEnd, "number", -1500));
|
|
482
|
+
p = hexEnd; // advance past hex digits
|
|
483
|
+
continue;
|
|
484
|
+
} else {
|
|
485
|
+
overrides.add(new Candidate(lastSlashIndex, lastSlashIndex + 2, "error", -2000));
|
|
486
|
+
p = p + 1; // move past 'u'
|
|
487
|
+
continue;
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// single-char escapes
|
|
492
|
+
String esc = String.valueOf(next);
|
|
493
|
+
if (VALID_ESCAPES.contains(esc)) {
|
|
494
|
+
// valid: highlight \X as "number"
|
|
495
|
+
overrides.add(new Candidate(lastSlashIndex, lastSlashIndex + 2, "number", -1500));
|
|
496
|
+
} else {
|
|
497
|
+
// invalid: highlight \X as "error"
|
|
498
|
+
overrides.add(new Candidate(lastSlashIndex, lastSlashIndex + 2, "error", -2000));
|
|
499
|
+
}
|
|
500
|
+
// advance past the escaped char
|
|
501
|
+
p = p + 1;
|
|
502
|
+
} else {
|
|
503
|
+
// even number of backslashes -> no escape for the following char
|
|
504
|
+
// continue scanning from that char
|
|
505
|
+
p = p + 1;
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
continue;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
// COMMENT: check any commentDefs that match at this index
|
|
513
|
+
boolean matchedCommentThisPos = false;
|
|
514
|
+
for (CommentDef cd : commentDefs) {
|
|
515
|
+
String s = cd.startsWith;
|
|
516
|
+
if (s == null || s.isEmpty()) continue;
|
|
517
|
+
if (line.startsWith(s, i)) {
|
|
518
|
+
int start = i;
|
|
519
|
+
if (cd.endsWith == null || cd.endsWith.isEmpty()) {
|
|
520
|
+
// single-line: rest of line is comment
|
|
521
|
+
pre.add(new Candidate(start, L, "comment", -1000));
|
|
522
|
+
i = L; // done with line
|
|
523
|
+
matchedCommentThisPos = true;
|
|
524
|
+
break;
|
|
525
|
+
} else {
|
|
526
|
+
// block comment that should end on the same line (simple mode)
|
|
527
|
+
int endIdx = line.indexOf(cd.endsWith, i + s.length());
|
|
528
|
+
if (endIdx == -1) {
|
|
529
|
+
pre.add(new Candidate(start, L, "comment", -1000));
|
|
530
|
+
i = L;
|
|
531
|
+
matchedCommentThisPos = true;
|
|
532
|
+
break;
|
|
533
|
+
} else {
|
|
534
|
+
int end = endIdx + cd.endsWith.length();
|
|
535
|
+
pre.add(new Candidate(start, end, "comment", -1000));
|
|
536
|
+
i = end;
|
|
537
|
+
matchedCommentThisPos = true;
|
|
538
|
+
break;
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
if (matchedCommentThisPos) continue;
|
|
544
|
+
|
|
545
|
+
// otherwise move forward
|
|
546
|
+
i++;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
// Add pre (strings/comments) into main candidate list
|
|
550
|
+
all.addAll(pre);
|
|
551
|
+
|
|
552
|
+
// We'll track a selected full-line background if any rule indicates it
|
|
553
|
+
Integer selectedLineBg = null;
|
|
554
|
+
int selectedLineBgPriority = Integer.MAX_VALUE;
|
|
555
|
+
|
|
556
|
+
// Run existing regex-based rules, skipping matches completely inside any pre-token
|
|
557
|
+
for (int ri = 0; ri < rules.size(); ri++) {
|
|
558
|
+
Rule r = rules.get(ri);
|
|
559
|
+
Matcher m = r.pattern.matcher(line);
|
|
560
|
+
while (m.find()) {
|
|
561
|
+
int ms = m.start();
|
|
562
|
+
int me = m.end();
|
|
563
|
+
if (ms < 0 || me <= ms) continue;
|
|
564
|
+
|
|
565
|
+
boolean insidePre = false;
|
|
566
|
+
for (Candidate pc : pre) {
|
|
567
|
+
if (ms >= pc.start && me <= pc.end) {
|
|
568
|
+
insidePre = true;
|
|
569
|
+
break;
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
if (insidePre) continue;
|
|
573
|
+
|
|
574
|
+
// If this rule has a lineBackground defined, mark the full line background
|
|
575
|
+
if (r.lineBackgroundColor != null) {
|
|
576
|
+
// prefer the rule with lowest priority index (earlier in file)
|
|
577
|
+
if (r.priority < selectedLineBgPriority) {
|
|
578
|
+
selectedLineBgPriority = r.priority;
|
|
579
|
+
selectedLineBg = r.lineBackgroundColor;
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
if (r.groupStyles != null && !r.groupStyles.isEmpty()) {
|
|
584
|
+
Iterator<Map.Entry<Integer, String>> it = r.groupStyles.entrySet().iterator();
|
|
585
|
+
while (it.hasNext()) {
|
|
586
|
+
Map.Entry<Integer, String> ge = it.next();
|
|
587
|
+
int gi = ge.getKey();
|
|
588
|
+
String style = ge.getValue();
|
|
589
|
+
try {
|
|
590
|
+
int gs = m.start(gi);
|
|
591
|
+
int gei = m.end(gi);
|
|
592
|
+
if (gs < 0 || gei <= gs) continue;
|
|
593
|
+
if (gs >= L) continue;
|
|
594
|
+
if (gei > L) gei = L;
|
|
595
|
+
all.add(new Candidate(gs, gei, style, r.priority));
|
|
596
|
+
} catch (Exception ex) {
|
|
597
|
+
// ignore missing group
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
} else if (r.type != null) {
|
|
601
|
+
int s = ms;
|
|
602
|
+
int e = me;
|
|
603
|
+
if (s < 0) s = 0;
|
|
604
|
+
if (e > L) e = L;
|
|
605
|
+
if (s >= e) continue;
|
|
606
|
+
all.add(new Candidate(s, e, r.type, r.priority));
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
// Sort candidates by priority, start, length
|
|
612
|
+
Collections.sort(all, new Comparator<Candidate>() {
|
|
613
|
+
public int compare(Candidate a, Candidate b) {
|
|
614
|
+
if (a.priority != b.priority) return a.priority - b.priority;
|
|
615
|
+
if (a.start != b.start) return a.start - b.start;
|
|
616
|
+
return b.length - a.length;
|
|
617
|
+
}
|
|
618
|
+
});
|
|
619
|
+
|
|
620
|
+
// Choose non-overlapping tokens for main candidates (strings/comments/other rules)
|
|
621
|
+
boolean[] taken = new boolean[L];
|
|
622
|
+
ArrayList<Token> chosen = new ArrayList<Token>();
|
|
623
|
+
for (int i2 = 0; i2 < all.size(); i2++) {
|
|
624
|
+
Candidate c = all.get(i2);
|
|
625
|
+
if (c.start < 0) c.start = 0;
|
|
626
|
+
if (c.end > L) c.end = L;
|
|
627
|
+
if (c.start >= c.end) continue;
|
|
628
|
+
boolean overlap = false;
|
|
629
|
+
for (int p = c.start; p < c.end; p++) {
|
|
630
|
+
if (taken[p]) {
|
|
631
|
+
overlap = true;
|
|
632
|
+
break;
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
if (overlap) continue;
|
|
636
|
+
|
|
637
|
+
Integer col = colors.get(c.style);
|
|
638
|
+
if (col == null) col = colors.get("default");
|
|
639
|
+
|
|
640
|
+
chosen.add(new Token(c.start, c.end, col.intValue()));
|
|
641
|
+
|
|
642
|
+
for (int p = c.start; p < c.end; p++) taken[p] = true;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
// Now add override tokens (escape/error) — they are allowed to overlap strings.
|
|
646
|
+
for (Candidate o : overrides) {
|
|
647
|
+
int s = o.start;
|
|
648
|
+
int e = o.end;
|
|
649
|
+
if (s < 0) s = 0;
|
|
650
|
+
if (e > L) e = L;
|
|
651
|
+
if (s >= e) continue;
|
|
652
|
+
Integer col = colors.get(o.style);
|
|
653
|
+
if (col == null) col = colors.get("default");
|
|
654
|
+
chosen.add(new Token(s, e, col.intValue()));
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
// Sort final tokens by start — but ensure longer (string) spans come before short overrides
|
|
658
|
+
// where same start
|
|
659
|
+
Collections.sort(chosen, new Comparator<Token>() {
|
|
660
|
+
public int compare(Token a, Token b) {
|
|
661
|
+
if (a.start != b.start) return a.start - b.start;
|
|
662
|
+
int lenA = a.end - a.start;
|
|
663
|
+
int lenB = b.end - b.start;
|
|
664
|
+
return lenB - lenA; // longer first
|
|
665
|
+
}
|
|
666
|
+
});
|
|
667
|
+
|
|
668
|
+
return new LineResult(chosen, selectedLineBg);
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
/** Draws text segments with their respective colors. */
|
|
672
|
+
// Fixed support for Arabic Letters by ChatGPT
|
|
673
|
+
private void renderTokens(Canvas canvas, String line, List<Token> tokens, int x, int y) {
|
|
674
|
+
if (line == null) return;
|
|
675
|
+
|
|
676
|
+
// Build full-line spannable
|
|
677
|
+
SpannableString ss = new SpannableString(line);
|
|
678
|
+
|
|
679
|
+
// 1) Apply default black for the entire line first (will be overridden by token spans)
|
|
680
|
+
ss.setSpan(
|
|
681
|
+
new ForegroundColorSpan(Color.BLACK),
|
|
682
|
+
0,
|
|
683
|
+
ss.length(),
|
|
684
|
+
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
|
|
685
|
+
);
|
|
686
|
+
|
|
687
|
+
// 2) Apply syntax colors from tokens (these override the default black)
|
|
688
|
+
if (tokens != null) {
|
|
689
|
+
for (Token t : tokens) {
|
|
690
|
+
try {
|
|
691
|
+
int start = Math.max(0, t.start);
|
|
692
|
+
int end = Math.min(line.length(), t.end);
|
|
693
|
+
if (start >= end) continue;
|
|
694
|
+
ss.setSpan(
|
|
695
|
+
new ForegroundColorSpan(t.color),
|
|
696
|
+
start,
|
|
697
|
+
end,
|
|
698
|
+
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
|
|
699
|
+
);
|
|
700
|
+
} catch (Exception ignore) {
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
// 3) Choose available width - measure full line width (ensure >=1)
|
|
706
|
+
float fullWidth = paint.measureText(line);
|
|
707
|
+
int availableWidth = Math.max(1, (int) Math.ceil(fullWidth));
|
|
708
|
+
|
|
709
|
+
// 4) Build StaticLayout ensuring LTR direction and no extra padding
|
|
710
|
+
StaticLayout layout = StaticLayout.Builder
|
|
711
|
+
.obtain(ss, 0, ss.length(), paint, availableWidth)
|
|
712
|
+
.setAlignment(Layout.Alignment.ALIGN_NORMAL) // left alignment for LTR flow
|
|
713
|
+
.setIncludePad(false)
|
|
714
|
+
.setTextDirection(TextDirectionHeuristics.LTR) // FORCE LTR for all text
|
|
715
|
+
.build();
|
|
716
|
+
|
|
717
|
+
// 5) Translate so that the layout's first baseline matches the 'y' baseline passed in.
|
|
718
|
+
// paint.getFontMetrics().top is the offset from baseline to the top of the text box.
|
|
719
|
+
Paint.FontMetrics fm = paint.getFontMetrics();
|
|
720
|
+
float topOffset = y + fm.top; // layout top such that baseline = y
|
|
721
|
+
|
|
722
|
+
canvas.save();
|
|
723
|
+
canvas.translate(x, topOffset);
|
|
724
|
+
layout.draw(canvas);
|
|
725
|
+
canvas.restore();
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
/** Removes a specific line’s cache entry */
|
|
729
|
+
public void clearLineCache(int lineIndex) {
|
|
730
|
+
synchronized (lineCache) {
|
|
731
|
+
lineCache.remove(lineIndex);
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
/** Safe substring extraction that prevents IndexOutOfBounds errors */
|
|
736
|
+
private String safeSubstring(String s, int start, int end) {
|
|
737
|
+
if (s == null) return "";
|
|
738
|
+
int len = s.length();
|
|
739
|
+
if (start < 0) start = 0;
|
|
740
|
+
if (end > len) end = len;
|
|
741
|
+
if (start >= end) return "";
|
|
742
|
+
return s.substring(start, end);
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
/** Debug helper: returns a list of token descriptions for inspection */
|
|
746
|
+
public List<String> debugTokenizeLine(String line) {
|
|
747
|
+
ArrayList<String> out = new ArrayList<String>();
|
|
748
|
+
ArrayList<Candidate> all = new ArrayList<Candidate>();
|
|
749
|
+
int L = (line == null) ? 0 : line.length();
|
|
750
|
+
if (L == 0) return out;
|
|
751
|
+
|
|
752
|
+
// Same logic as tokenizeLine(), but returns descriptive strings
|
|
753
|
+
Integer selectedLineBg = null;
|
|
754
|
+
int selectedLineBgPriority = Integer.MAX_VALUE;
|
|
755
|
+
|
|
756
|
+
for (int ri = 0; ri < rules.size(); ri++) {
|
|
757
|
+
Rule r = rules.get(ri);
|
|
758
|
+
Matcher m = r.pattern.matcher(line);
|
|
759
|
+
while (m.find()) {
|
|
760
|
+
int ms = m.start();
|
|
761
|
+
int me = m.end();
|
|
762
|
+
if (ms < 0 || me <= ms) continue;
|
|
763
|
+
// collect group styles or full match as candidates (no pre skipping here for debug)
|
|
764
|
+
if (r.lineBackgroundColor != null) {
|
|
765
|
+
if (r.priority < selectedLineBgPriority) {
|
|
766
|
+
selectedLineBgPriority = r.priority;
|
|
767
|
+
selectedLineBg = r.lineBackgroundColor;
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
if (r.groupStyles != null && !r.groupStyles.isEmpty()) {
|
|
771
|
+
Iterator<Map.Entry<Integer, String>> it = r.groupStyles.entrySet().iterator();
|
|
772
|
+
while (it.hasNext()) {
|
|
773
|
+
Map.Entry<Integer, String> ge = it.next();
|
|
774
|
+
int gi = ge.getKey();
|
|
775
|
+
String style = ge.getValue();
|
|
776
|
+
try {
|
|
777
|
+
int gs = m.start(gi);
|
|
778
|
+
int gei = m.end(gi);
|
|
779
|
+
if (gs < 0 || gei <= gs) continue;
|
|
780
|
+
if (gs >= L) continue;
|
|
781
|
+
if (gei > L) gei = L;
|
|
782
|
+
all.add(new Candidate(gs, gei, style, r.priority));
|
|
783
|
+
} catch (Exception ex) {
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
} else if (r.type != null) {
|
|
787
|
+
int s = ms;
|
|
788
|
+
int e = me;
|
|
789
|
+
if (s < 0) s = 0;
|
|
790
|
+
if (e > L) e = L;
|
|
791
|
+
if (s >= e) continue;
|
|
792
|
+
all.add(new Candidate(s, e, r.type, r.priority));
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
// Sort and select non-overlapping matches
|
|
798
|
+
Collections.sort(all, new Comparator<Candidate>() {
|
|
799
|
+
public int compare(Candidate a, Candidate b) {
|
|
800
|
+
if (a.priority != b.priority) return a.priority - b.priority;
|
|
801
|
+
if (a.start != b.start) return a.start - b.start;
|
|
802
|
+
return b.length - a.length;
|
|
803
|
+
}
|
|
804
|
+
});
|
|
805
|
+
|
|
806
|
+
boolean[] taken = new boolean[L];
|
|
807
|
+
for (int i = 0; i < all.size(); i++) {
|
|
808
|
+
Candidate c = all.get(i);
|
|
809
|
+
if (c.start < 0) c.start = 0;
|
|
810
|
+
if (c.end > L) c.end = L;
|
|
811
|
+
if (c.start >= c.end) continue;
|
|
812
|
+
boolean overlap = false;
|
|
813
|
+
for (int p = c.start; p < c.end; p++) {
|
|
814
|
+
if (taken[p]) {
|
|
815
|
+
overlap = true;
|
|
816
|
+
break;
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
if (overlap) continue;
|
|
820
|
+
|
|
821
|
+
// Add readable debug output
|
|
822
|
+
out.add(String.format("tok[%d,%d] pr=%d style=%s text=\"%s\"", c.start, c.end, c.priority, c.style, safeSubstring(line, c.start, c.end)));
|
|
823
|
+
for (int p = c.start; p < c.end; p++) taken[p] = true;
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
if (selectedLineBg != null) {
|
|
827
|
+
out.add(String.format("LINE-BG color=%s", String.format("#%06X", (0xFFFFFF & selectedLineBg))));
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
return out;
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
/** Logs debug information for tokenized line to Logcat */
|
|
834
|
+
public void logDebugTokens(String line) {
|
|
835
|
+
List<String> t = debugTokenizeLine(line);
|
|
836
|
+
for (int i = 0; i < t.size(); i++) {
|
|
837
|
+
Log.d(TAG, t.get(i));
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
}
|