From eef6e132080c5e46ba8c47ecfaca83fa8e0e214e Mon Sep 17 00:00:00 2001 From: Benjamin Culkin Date: Wed, 28 Jan 2026 21:36:12 -0500 Subject: Add various text UI components This adds a variety of text UI components, namely two suites: * One that is geared towards JSON * One that is geared towards Markdown Details to (perhaps) follow later --- .../src/main/java/bjc/utils/gui/JsonEditorKit.java | 899 +++++++++++++++++++++ 1 file changed, 899 insertions(+) create mode 100644 base/src/main/java/bjc/utils/gui/JsonEditorKit.java (limited to 'base/src/main/java/bjc/utils/gui/JsonEditorKit.java') diff --git a/base/src/main/java/bjc/utils/gui/JsonEditorKit.java b/base/src/main/java/bjc/utils/gui/JsonEditorKit.java new file mode 100644 index 0000000..46519cf --- /dev/null +++ b/base/src/main/java/bjc/utils/gui/JsonEditorKit.java @@ -0,0 +1,899 @@ +package bjc.utils.gui; +import javax.swing.*; +import javax.swing.event.ChangeListener; +import javax.swing.text.*; +import java.awt.*; +import java.awt.event.ComponentAdapter; +import java.awt.event.ComponentEvent; +import java.io.*; +import java.util.Arrays; + +/** + * JSON syntax highlighting for JTextPane via StyledEditorKit/StyledDocument. + * + * Includes: + * 1) Highlights only the visible region (plus margin) for performance. + * 2) Rainbow brace/bracket coloring: each matching pair shares a color by nesting depth. + * 3) Caret pair emphasis: when caret is on/after a brace, emphasize it + its matching pair. + * 4) Underlines mismatched/unpaired braces/brackets. + * + * Usage: + * JTextPane pane = new JTextPane(); + * pane.setEditorKit(new JsonEditorKit()); + * JsonEditorKit.JsonHighlightSupport.install(pane); + * ((JsonEditorKit.JsonDocument)pane.getDocument()).setJson(jsonString); + */ +public class JsonEditorKit extends StyledEditorKit { + private static final long serialVersionUID = 7378968732241012969L; + + @Override + public String getContentType() { + return "application/json"; + } + + @Override + public Document createDefaultDocument() { + return new JsonDocument(); + } + + /** + * Set the JSON contents of this document and a text pane + * + * @param pane The text pane + * @param json The JSON document + */ + public void setJson(JTextPane pane, String json) { + if (!(pane.getDocument() instanceof JsonDocument)) { + pane.setDocument(createDefaultDocument()); + } + ((JsonDocument) pane.getDocument()).setJson(json); + } + + @Override + public void read(Reader in, Document doc, int pos) throws IOException, BadLocationException { + String json = readAll(in); + if (doc instanceof JsonDocument) { + ((JsonDocument) doc).insertJsonAt(pos, json); + } else if (doc instanceof StyledDocument) { + StyledDocument sd = (StyledDocument) doc; + sd.insertString(pos, json, null); + JsonHighlighter.highlight(sd, 0, sd.getLength(), JsonStyles.defaults(), null, -1); + } else { + super.read(new StringReader(json), doc, pos); + } + } + + @Override + public void write(Writer out, Document doc, int pos, int len) throws IOException, BadLocationException { + out.write(doc.getText(pos, len)); + } + + private static String readAll(Reader r) throws IOException { + StringBuilder sb = new StringBuilder(4096); + char[] buf = new char[4096]; + int n; + while ((n = r.read(buf)) >= 0) + sb.append(buf, 0, n); + return sb.toString(); + } + + // ------------------------------------------------------------------------- + // Install helper: visible-region updates + caret updates + // ------------------------------------------------------------------------- + + /** + * Helper for doing JSON highlighting + */ + public static final class JsonHighlightSupport { + private static final String INSTALLED_KEY = "json.highlighter.installed"; + private static final int DEFAULT_MARGIN_CHARS = 2048; + + private JsonHighlightSupport() { + } + + /** + * Install the JSON highlighter into a pane + * + * @param pane The pane to install the highlighter into + */ + public static void install(JTextPane pane) { + if (pane == null) + throw new IllegalArgumentException("pane == null"); + if (Boolean.TRUE.equals(pane.getClientProperty(INSTALLED_KEY))) + return; + pane.putClientProperty(INSTALLED_KEY, Boolean.TRUE); + + if (!(pane.getDocument() instanceof JsonDocument)) { + pane.setEditorKit(new JsonEditorKit()); + } + + final JsonDocument doc = (JsonDocument) pane.getDocument(); + doc.attachView(pane); + doc.setVisibleHighlightMargin(DEFAULT_MARGIN_CHARS); + + // Re-highlight on resize + pane.addComponentListener(new ComponentAdapter() { + @Override + public void componentResized(ComponentEvent e) { + doc.queueHighlight(); + } + }); + + // Re-highlight on caret moves (pair emphasis) + pane.addCaretListener(e -> doc.queueHighlight()); + + // Re-highlight on viewport scrolling + final Runnable hookViewport = () -> { + JViewport vp = (JViewport) SwingUtilities.getAncestorOfClass(JViewport.class, pane); + if (vp == null) + return; + + final String VP_LISTENER_KEY = "json.highlighter.viewport.listener"; + if (vp.getClientProperty(VP_LISTENER_KEY) != null) + return; + + ChangeListener cl = e -> doc.queueHighlight(); + vp.addChangeListener(cl); + vp.putClientProperty(VP_LISTENER_KEY, cl); + }; + + pane.addHierarchyListener(e -> hookViewport.run()); + hookViewport.run(); + + doc.queueHighlight(); + } + } + + // ------------------------------------------------------------------------- + // Document + // ------------------------------------------------------------------------- + + public static class JsonDocument extends DefaultStyledDocument { + private final JsonStyles styles = JsonStyles.defaults(); + + private transient JTextComponent view; + + private boolean highlightQueued = false; + private int visibleMarginChars = 2048; + + // Recompute rainbow pairing only when content changes + private boolean structureDirty = true; + private JsonHighlighter.RainbowData rainbowData = null; + + public JsonDocument() { + super(); + + addDocumentListener(new javax.swing.event.DocumentListener() { + @Override + public void insertUpdate(javax.swing.event.DocumentEvent e) { + onContentChanged(); + } + + @Override + public void removeUpdate(javax.swing.event.DocumentEvent e) { + onContentChanged(); + } + + @Override + public void changedUpdate(javax.swing.event.DocumentEvent e) { + /* ignore attribute changes */ } + }); + } + + public void attachView(JTextComponent view) { + this.view = view; + } + + public void setVisibleHighlightMargin(int chars) { + this.visibleMarginChars = Math.max(0, chars); + } + + public void setJson(String json) { + try { + remove(0, getLength()); + insertString(0, json, null); + } catch (BadLocationException e) { + throw new RuntimeException(e); + } + } + + public void insertJsonAt(int pos, String json) throws BadLocationException { + insertString(pos, json, null); + } + + private void onContentChanged() { + structureDirty = true; + queueHighlight(); + } + + void queueHighlight() { + if (highlightQueued) + return; + highlightQueued = true; + + SwingUtilities.invokeLater(() -> { + highlightQueued = false; + try { + doHighlight(); + } catch (BadLocationException ignored) { + } + }); + } + + private void doHighlight() throws BadLocationException { + int docLen = getLength(); + if (docLen <= 0) + return; + + // Recompute rainbow mapping when content changed + if (structureDirty) { + String fullText = getText(0, docLen); + rainbowData = JsonHighlighter.computeRainbow(fullText, styles.rainbowOverlays.length); + structureDirty = false; + } + + int start = 0; + int end = docLen; + int caretPos = -1; + + if (view != null && view.isShowing()) { + caretPos = view.getCaretPosition(); + int[] range = computeVisibleRange(view, docLen, visibleMarginChars); + start = range[0]; + end = range[1]; + } + + int len = Math.max(0, end - start); + if (len <= 0) + return; + + // Determine whether caret is on/after a brace/bracket + int activeBraceOffset = -1; + if (caretPos >= 0 && rainbowData != null && rainbowData.offsets.length > 0) { + activeBraceOffset = findBraceAtCaret(this, caretPos, rainbowData); + } + + JsonHighlighter.highlight(this, start, len, styles, rainbowData, activeBraceOffset); + } + + private static int findBraceAtCaret(StyledDocument doc, int caretPos, JsonHighlighter.RainbowData rd) { + try { + int docLen = doc.getLength(); + + // Prefer caret-1 (common editor behavior: caret after brace highlights it) + int[] candidates = new int[] { caretPos - 1, caretPos }; + for (int off : candidates) { + if (off < 0 || off >= docLen) + continue; + + char ch = doc.getText(off, 1).charAt(0); + if (!(ch == '{' || ch == '}' || ch == '[' || ch == ']')) + continue; + + // Only accept if this brace is actually part of the parsed structure + // (i.e., not in string/comment), which is represented by rd.offsets. + int idx = JsonHighlighter.lowerBound(rd.offsets, off); + if (idx < rd.offsets.length && rd.offsets[idx] == off) + return off; + } + } catch (BadLocationException ignored) { + } + + return -1; + } + + private static int[] computeVisibleRange(JTextComponent comp, int docLen, int marginChars) { + Rectangle r = comp.getVisibleRect(); + Point p1 = new Point(r.x, r.y); + Point p2 = new Point(r.x + r.width, r.y + r.height); + + int a = safeViewToModel(comp, p1); + int b = safeViewToModel(comp, p2); + + int start = Math.max(0, Math.min(a, b)); + int end = Math.min(docLen, Math.max(a, b)); + + start = Math.max(0, start - marginChars); + end = Math.min(docLen, end + marginChars); + + if (end < start) { + int t = start; + start = end; + end = t; + } + return new int[] { start, end }; + } + + @SuppressWarnings("deprecation") // Java 8 compatible + private static int safeViewToModel(JTextComponent comp, Point p) { + try { + return Math.max(0, comp.viewToModel(p)); + } catch (Exception e) { + return 0; + } + } + } + + // ------------------------------------------------------------------------- + // Styles + // ------------------------------------------------------------------------- + + public static final class JsonStyles { + public final SimpleAttributeSet base = new SimpleAttributeSet(); + + public final SimpleAttributeSet key; + public final SimpleAttributeSet string; + public final SimpleAttributeSet number; + public final SimpleAttributeSet boolNull; + public final SimpleAttributeSet punctuation; + public final SimpleAttributeSet comment; + + /** Rainbow overlays for braces/brackets (apply with replace=false). */ + public final SimpleAttributeSet[] rainbowOverlays; + + /** For mismatches/unpaired (apply with replace=false). */ + public final SimpleAttributeSet mismatchOverlay; + + /** Caret brace emphasis (stronger). */ + public final SimpleAttributeSet caretBraceOverlay; + + /** Matching brace emphasis (weaker). */ + public final SimpleAttributeSet matchBraceOverlay; + + private JsonStyles(SimpleAttributeSet key, SimpleAttributeSet string, SimpleAttributeSet number, + SimpleAttributeSet boolNull, SimpleAttributeSet punctuation, SimpleAttributeSet comment, + SimpleAttributeSet[] rainbowOverlays, SimpleAttributeSet mismatchOverlay, + SimpleAttributeSet caretBraceOverlay, SimpleAttributeSet matchBraceOverlay) { + this.key = key; + this.string = string; + this.number = number; + this.boolNull = boolNull; + this.punctuation = punctuation; + this.comment = comment; + this.rainbowOverlays = rainbowOverlays; + this.mismatchOverlay = mismatchOverlay; + this.caretBraceOverlay = caretBraceOverlay; + this.matchBraceOverlay = matchBraceOverlay; + } + + public static JsonStyles defaults() { + // Base + SimpleAttributeSet base = new SimpleAttributeSet(); + StyleConstants.setFontFamily(base, "Monospaced"); + StyleConstants.setFontSize(base, 13); + + SimpleAttributeSet key = cloneWith(base); + StyleConstants.setForeground(key, new Color(0, 92, 197)); + StyleConstants.setBold(key, true); + + SimpleAttributeSet string = cloneWith(base); + StyleConstants.setForeground(string, new Color(34, 139, 34)); + + SimpleAttributeSet number = cloneWith(base); + StyleConstants.setForeground(number, new Color(163, 73, 164)); + + SimpleAttributeSet boolNull = cloneWith(base); + StyleConstants.setForeground(boolNull, new Color(184, 92, 0)); + StyleConstants.setBold(boolNull, true); + + SimpleAttributeSet punctuation = cloneWith(base); + StyleConstants.setForeground(punctuation, new Color(120, 120, 120)); + + SimpleAttributeSet comment = cloneWith(base); + StyleConstants.setForeground(comment, new Color(140, 140, 140)); + StyleConstants.setItalic(comment, true); + + // Rainbow palette (light background friendly) + Color[] palette = new Color[] { new Color(216, 27, 96), // pink/red + new Color(255, 143, 0), // orange + new Color(251, 192, 45), // amber + new Color(67, 160, 71), // green + new Color(0, 172, 193), // cyan + new Color(30, 136, 229), // blue + new Color(94, 53, 177), // deep purple + new Color(142, 36, 170) // violet + }; + + SimpleAttributeSet[] rainbow = new SimpleAttributeSet[palette.length]; + for (int i = 0; i < palette.length; i++) { + SimpleAttributeSet o = new SimpleAttributeSet(); + StyleConstants.setForeground(o, palette[i]); + StyleConstants.setBold(o, true); + rainbow[i] = o; + } + + // Mismatch: underline red + red foreground + SimpleAttributeSet mismatch = new SimpleAttributeSet(); + StyleConstants.setUnderline(mismatch, true); + StyleConstants.setForeground(mismatch, new Color(200, 0, 0)); + StyleConstants.setBold(mismatch, true); + + // Caret brace emphasis (strong): background + underline + SimpleAttributeSet caretEm = new SimpleAttributeSet(); + StyleConstants.setBackground(caretEm, new Color(255, 242, 179)); // slightly stronger yellow + StyleConstants.setUnderline(caretEm, true); + StyleConstants.setBold(caretEm, true); + + // Matching brace emphasis (soft): background only + SimpleAttributeSet matchEm = new SimpleAttributeSet(); + StyleConstants.setBackground(matchEm, new Color(255, 250, 210)); // softer yellow + StyleConstants.setBold(matchEm, true); + + JsonStyles s = new JsonStyles(key, string, number, boolNull, punctuation, comment, rainbow, mismatch, + caretEm, matchEm); + s.base.addAttributes(base); + return s; + } + + private static SimpleAttributeSet cloneWith(SimpleAttributeSet base) { + SimpleAttributeSet a = new SimpleAttributeSet(); + a.addAttributes(base); + return a; + } + } + + // ------------------------------------------------------------------------- + // Highlighter / Lexer + Rainbow Pairing + Caret emphasis + // ------------------------------------------------------------------------- + + static final class JsonHighlighter { + + /** + * Rainbow mapping: - offsets: positions of braces/brackets to color (sorted) - + * colorIndex: palette index for that brace/bracket - match: matching brace + * offset (or -1 if unpaired) - mismatches: offsets of unpaired/mismatched + * braces/brackets (sorted) + */ + static final class RainbowData { + final int[] offsets; + final byte[] colorIndex; + final int[] match; + final int[] mismatches; + + RainbowData(int[] offsets, byte[] colorIndex, int[] match, int[] mismatches) { + this.offsets = offsets; + this.colorIndex = colorIndex; + this.match = match; + this.mismatches = mismatches; + } + } + + static void highlight(StyledDocument doc, int start, int length, JsonStyles styles, RainbowData rainbowData, + int activeBraceOffset) throws BadLocationException { + + if (length <= 0) + return; + + String text = doc.getText(start, length); + + // Reset region + doc.setCharacterAttributes(start, length, styles.base, true); + + int i = 0; + final int n = text.length(); + + while (i < n) { + char c = text.charAt(i); + + if (isWs(c)) { + i++; + continue; + } + + // punctuation + if (isPunct(c)) { + apply(doc, start + i, 1, styles.punctuation, true); + i++; + continue; + } + + // comments (JSONC-ish): //... or /*...*/ + if (c == '/' && i + 1 < n) { + char d = text.charAt(i + 1); + if (d == '/') { + int j = i + 2; + while (j < n && text.charAt(j) != '\n') + j++; + apply(doc, start + i, j - i, styles.comment, true); + i = j; + continue; + } + if (d == '*') { + int j = i + 2; + boolean closed = false; + while (j + 1 < n) { + if (text.charAt(j) == '*' && text.charAt(j + 1) == '/') { + j += 2; + closed = true; + break; + } + j++; + } + if (closed) { + apply(doc, start + i, j - i, styles.comment, true); + i = j; + continue; + } else { + apply(doc, start + i, n - i, styles.mismatchOverlay, false); + break; + } + } + } + + // string + if (c == '"') { + int j = i + 1; + boolean escaped = false; + boolean closed = false; + + while (j < n) { + char ch = text.charAt(j); + if (escaped) { + escaped = false; + } else if (ch == '\\') { + escaped = true; + } else if (ch == '"') { + j++; + closed = true; + break; + } + j++; + } + + if (!closed) { + apply(doc, start + i, n - i, styles.mismatchOverlay, false); + break; + } + + // Key detection: string followed by ':' after whitespace + int k = j; + while (k < n && isWs(text.charAt(k))) + k++; + boolean isKey = (k < n && text.charAt(k) == ':'); + + apply(doc, start + i, j - i, isKey ? styles.key : styles.string, true); + i = j; + continue; + } + + // keywords: true false null + if (isAlpha(c)) { + int j = i + 1; + while (j < n && isAlpha(text.charAt(j))) + j++; + String word = text.substring(i, j); + if ("true".equals(word) || "false".equals(word) || "null".equals(word)) { + apply(doc, start + i, j - i, styles.boolNull, true); + } + i = j; + continue; + } + + // number + if (c == '-' || isDigit(c)) { + int j = scanNumber(text, i); + if (j > i) { + apply(doc, start + i, j - i, styles.number, true); + i = j; + continue; + } + } + + i++; + } + + // Apply rainbow overlays for braces/brackets within region + if (rainbowData != null && rainbowData.offsets.length > 0) { + int regionStart = start; + int regionEnd = start + length; + + int idx = lowerBound(rainbowData.offsets, regionStart); + while (idx < rainbowData.offsets.length) { + int off = rainbowData.offsets[idx]; + if (off >= regionEnd) + break; + + int paletteIdx = (rainbowData.colorIndex[idx] & 0xFF) % styles.rainbowOverlays.length; + apply(doc, off, 1, styles.rainbowOverlays[paletteIdx], false); + + idx++; + } + } + + // Apply mismatch underline on top (will override rainbow to red) + if (rainbowData != null && rainbowData.mismatches.length > 0) { + int regionStart = start; + int regionEnd = start + length; + + int idx = lowerBound(rainbowData.mismatches, regionStart); + while (idx < rainbowData.mismatches.length) { + int off = rainbowData.mismatches[idx]; + if (off >= regionEnd) + break; + apply(doc, off, 1, styles.mismatchOverlay, false); + idx++; + } + } + + // Caret pair emphasis (on top of rainbow; mismatch still wins visually if + // present) + if (rainbowData != null && activeBraceOffset >= 0) { + int idx = lowerBound(rainbowData.offsets, activeBraceOffset); + if (idx < rainbowData.offsets.length && rainbowData.offsets[idx] == activeBraceOffset) { + int matchOff = rainbowData.match[idx]; + if (matchOff >= 0) { + int regionStart = start; + int regionEnd = start + length; + + // Emphasize active brace strongly + if (activeBraceOffset >= regionStart && activeBraceOffset < regionEnd) { + apply(doc, activeBraceOffset, 1, styles.caretBraceOverlay, false); + } + // Emphasize matching brace softly + if (matchOff >= regionStart && matchOff < regionEnd) { + apply(doc, matchOff, 1, styles.matchBraceOverlay, false); + } + } + } + } + } + + /** + * Compute rainbow coloring for matching {} and [] pairs using nesting depth. + * Skips braces/brackets in strings and (optional) JSONC comments. + * + * Openers get color by current depth; closers get their opener's color. + * Unpaired/mismatched braces are recorded in mismatches; openers keep their + * rainbow color even if unclosed. + */ + static RainbowData computeRainbow(String text, int paletteSize) { + if (text == null || text.isEmpty()) + return new RainbowData(new int[0], new byte[0], new int[0], new int[0]); + if (paletteSize <= 0) + paletteSize = 1; + + final int n = text.length(); + + // mapping arrays (offsets sorted by scan) + int[] offsets = new int[Math.min(4096, n)]; + byte[] colors = new byte[offsets.length]; + int[] match = new int[offsets.length]; + int count = 0; + + // mismatches (cap) + int capMismatch = 512; + int[] mism = new int[capMismatch]; + int mismCount = 0; + + // stack: store opener type + opener entry index + char[] stCh = new char[Math.min(1024, n)]; + int[] stEntryIdx = new int[stCh.length]; + int[] stPos = new int[stCh.length]; + int top = 0; + + boolean inString = false; + boolean escaped = false; + boolean inLineComment = false; + boolean inBlockComment = false; + + for (int i = 0; i < n; i++) { + char c = text.charAt(i); + + if (inLineComment) { + if (c == '\n') + inLineComment = false; + continue; + } + if (inBlockComment) { + if (c == '*' && i + 1 < n && text.charAt(i + 1) == '/') { + inBlockComment = false; + i++; + } + continue; + } + + if (inString) { + if (escaped) { + escaped = false; + } else if (c == '\\') { + escaped = true; + } else if (c == '"') { + inString = false; + } + continue; + } else if (c == '"') { + inString = true; + escaped = false; + continue; + } + + // JSONC-ish comments + if (c == '/' && i + 1 < n) { + char d = text.charAt(i + 1); + if (d == '/') { + inLineComment = true; + i++; + continue; + } + if (d == '*') { + inBlockComment = true; + i++; + continue; + } + } + + // openers + if (c == '{' || c == '[') { + byte ci = (byte) (top % paletteSize); + + // ensure mapping arrays capacity + if (count >= offsets.length) { + int newLen = Math.min(n, offsets.length * 2); + offsets = Arrays.copyOf(offsets, newLen); + colors = Arrays.copyOf(colors, newLen); + match = Arrays.copyOf(match, newLen); + } + + offsets[count] = i; + colors[count] = ci; + match[count] = -1; // fill later + int entryIdx = count; + count++; + + // push onto stack + if (top >= stCh.length) { + int newLen = Math.min(n, stCh.length * 2); + stCh = Arrays.copyOf(stCh, newLen); + stEntryIdx = Arrays.copyOf(stEntryIdx, newLen); + stPos = Arrays.copyOf(stPos, newLen); + } + + stCh[top] = c; + stEntryIdx[top] = entryIdx; + stPos[top] = i; + top++; + continue; + } + + // closers + if (c == '}' || c == ']') { + if (top == 0) { + if (mismCount < capMismatch) + mism[mismCount++] = i; + continue; + } + + char open = stCh[top - 1]; + int openEntryIdx = stEntryIdx[top - 1]; + + boolean matchType = (open == '{' && c == '}') || (open == '[' && c == ']'); + if (!matchType) { + if (mismCount < capMismatch) + mism[mismCount++] = i; + // recovery: pop one to reduce cascading errors + top--; + continue; + } + + // matching pair + top--; + + // ensure mapping arrays capacity for closer entry + if (count >= offsets.length) { + int newLen = Math.min(n, offsets.length * 2); + offsets = Arrays.copyOf(offsets, newLen); + colors = Arrays.copyOf(colors, newLen); + match = Arrays.copyOf(match, newLen); + } + + byte ci = colors[openEntryIdx]; + + offsets[count] = i; + colors[count] = ci; + match[count] = offsets[openEntryIdx]; // closer -> opener + match[openEntryIdx] = i; // opener -> closer + count++; + continue; + } + } + + // leftover openers are mismatches (their match stays -1) + while (top > 0 && mismCount < capMismatch) { + top--; + mism[mismCount++] = stPos[top]; + } + + int[] outOffsets = Arrays.copyOf(offsets, count); + byte[] outColors = Arrays.copyOf(colors, count); + int[] outMatch = Arrays.copyOf(match, count); + + int[] outMism = Arrays.copyOf(mism, mismCount); + Arrays.sort(outMism); + + return new RainbowData(outOffsets, outColors, outMatch, outMism); + } + + // --- helpers --- + + private static void apply(StyledDocument doc, int offset, int len, AttributeSet style, boolean replace) { + doc.setCharacterAttributes(offset, len, style, replace); + } + + private static boolean isWs(char c) { + return c == ' ' || c == '\t' || c == '\r' || c == '\n' || c == '\f'; + } + + private static boolean isDigit(char c) { + return c >= '0' && c <= '9'; + } + + private static boolean isAlpha(char c) { + return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z'); + } + + private static boolean isPunct(char c) { + return c == '{' || c == '}' || c == '[' || c == ']' || c == ':' || c == ','; + } + + static int lowerBound(int[] a, int key) { + int lo = 0, hi = a.length; + while (lo < hi) { + int mid = (lo + hi) >>> 1; + if (a[mid] < key) + lo = mid + 1; + else + hi = mid; + } + return lo; + } + + /** + * JSON number: -? (0|[1-9]\d*) (\.\d+)? ([eE][+-]?\d+)? Returns end index + * (exclusive). If not a valid prefix, returns i. + */ + private static int scanNumber(String s, int i) { + int n = s.length(); + int j = i; + + if (j < n && s.charAt(j) == '-') + j++; + + int intStart = j; + + if (j < n && s.charAt(j) == '0') { + j++; + } else { + if (j >= n || !isDigit(s.charAt(j))) + return i; + while (j < n && isDigit(s.charAt(j))) + j++; + } + + // fraction + if (j < n && s.charAt(j) == '.') { + int dot = j; + j++; + if (j >= n || !isDigit(s.charAt(j))) + return dot; + while (j < n && isDigit(s.charAt(j))) + j++; + } + + // exponent + if (j < n && (s.charAt(j) == 'e' || s.charAt(j) == 'E')) { + int epos = j; + j++; + if (j < n && (s.charAt(j) == '+' || s.charAt(j) == '-')) + j++; + if (j >= n || !isDigit(s.charAt(j))) + return epos; + while (j < n && isDigit(s.charAt(j))) + j++; + } + + if (j == intStart) + return i; + return j; + } + } +} \ No newline at end of file -- cgit v1.2.3