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.
Files changed (66) hide show
  1. package/android/src/main/AndroidManifest.xml +8 -0
  2. package/android/src/main/assets/availableSyntax.json +54 -0
  3. package/android/src/main/assets/c.json +42 -0
  4. package/android/src/main/assets/colors.json +21 -0
  5. package/android/src/main/assets/cpp.json +79 -0
  6. package/android/src/main/assets/dart.json +108 -0
  7. package/android/src/main/assets/java.json +46 -0
  8. package/android/src/main/assets/js.json +54 -0
  9. package/android/src/main/assets/json.json +33 -0
  10. package/android/src/main/assets/kotlin.json +53 -0
  11. package/android/src/main/assets/lua.json +54 -0
  12. package/android/src/main/assets/php.json +69 -0
  13. package/android/src/main/assets/python.json +87 -0
  14. package/android/src/main/assets/rust.json +139 -0
  15. package/android/src/main/assets/smali.json +131 -0
  16. package/android/src/main/assets/xml.json +96 -0
  17. package/android/src/main/java/com/aetherlink/dexeditor/DexEditorPluginPlugin.java +48 -0
  18. package/android/src/main/java/com/aetherlink/dexeditor/SmaliEditorActivity.java +205 -0
  19. package/android/src/main/java/com/aetherlink/dexeditor/editor/EditView.java +4022 -0
  20. package/android/src/main/java/com/aetherlink/dexeditor/editor/WordWrapLayout.java +275 -0
  21. package/android/src/main/java/com/aetherlink/dexeditor/editor/buffer/BufferCache.java +113 -0
  22. package/android/src/main/java/com/aetherlink/dexeditor/editor/buffer/GapBuffer.java +685 -0
  23. package/android/src/main/java/com/aetherlink/dexeditor/editor/component/ClipboardPanel.java +1380 -0
  24. package/android/src/main/java/com/aetherlink/dexeditor/editor/component/Magnifier.java +363 -0
  25. package/android/src/main/java/com/aetherlink/dexeditor/editor/highlight/Candidate.java +52 -0
  26. package/android/src/main/java/com/aetherlink/dexeditor/editor/highlight/CommentDef.java +47 -0
  27. package/android/src/main/java/com/aetherlink/dexeditor/editor/highlight/LineResult.java +49 -0
  28. package/android/src/main/java/com/aetherlink/dexeditor/editor/highlight/MHSyntaxHighlightEngine.java +841 -0
  29. package/android/src/main/java/com/aetherlink/dexeditor/editor/highlight/Rule.java +53 -0
  30. package/android/src/main/java/com/aetherlink/dexeditor/editor/highlight/Token.java +48 -0
  31. package/android/src/main/java/com/aetherlink/dexeditor/editor/listener/OnTextChangedListener.java +6 -0
  32. package/android/src/main/java/com/aetherlink/dexeditor/editor/utils/Pair.java +80 -0
  33. package/android/src/main/java/com/aetherlink/dexeditor/editor/utils/ScreenUtils.java +63 -0
  34. package/android/src/main/res/drawable/abc_text_cursor_material.xml +9 -0
  35. package/android/src/main/res/drawable/abc_text_select_handle_left_mtrl.png +0 -0
  36. package/android/src/main/res/drawable/abc_text_select_handle_middle_mtrl.png +0 -0
  37. package/android/src/main/res/drawable/abc_text_select_handle_right_mtrl.png +0 -0
  38. package/android/src/main/res/drawable/ic_arrow_back.xml +12 -0
  39. package/android/src/main/res/drawable/ic_copy.xml +11 -0
  40. package/android/src/main/res/drawable/ic_cut.xml +10 -0
  41. package/android/src/main/res/drawable/ic_delete.xml +12 -0
  42. package/android/src/main/res/drawable/ic_edit_white_24dp.xml +10 -0
  43. package/android/src/main/res/drawable/ic_goto.xml +10 -0
  44. package/android/src/main/res/drawable/ic_launcher_background.xml +186 -0
  45. package/android/src/main/res/drawable/ic_look_white_24dp.xml +11 -0
  46. package/android/src/main/res/drawable/ic_more.xml +10 -0
  47. package/android/src/main/res/drawable/ic_open_link.xml +11 -0
  48. package/android/src/main/res/drawable/ic_paste.xml +11 -0
  49. package/android/src/main/res/drawable/ic_redo_white_24dp.xml +10 -0
  50. package/android/src/main/res/drawable/ic_select.xml +11 -0
  51. package/android/src/main/res/drawable/ic_select_all.xml +10 -0
  52. package/android/src/main/res/drawable/ic_share.xml +11 -0
  53. package/android/src/main/res/drawable/ic_toggle_comment.xml +12 -0
  54. package/android/src/main/res/drawable/ic_translate.xml +10 -0
  55. package/android/src/main/res/drawable/ic_undo_white_24dp.xml +11 -0
  56. package/android/src/main/res/drawable/magnifier_bg.xml +5 -0
  57. package/android/src/main/res/drawable/popup_background.xml +8 -0
  58. package/android/src/main/res/drawable/ripple_effect.xml +9 -0
  59. package/android/src/main/res/drawable/selection_menu_background.xml +5 -0
  60. package/android/src/main/res/layout/custom_selection_menu.xml +44 -0
  61. package/android/src/main/res/layout/expand_button.xml +23 -0
  62. package/android/src/main/res/layout/item_autocomplete.xml +25 -0
  63. package/android/src/main/res/layout/magnifier_popup.xml +17 -0
  64. package/android/src/main/res/layout/menu_item.xml +30 -0
  65. package/android/src/main/res/layout/text_selection_menu.xml +36 -0
  66. package/package.json +1 -1
@@ -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
+ }