|
| 1 | +package json.java21.jdt; |
| 2 | + |
| 3 | +import jdk.sandbox.java.util.json.JsonValue; |
| 4 | +import json.java21.jdt.JdtAst.*; |
| 5 | + |
| 6 | +import java.util.Map; |
| 7 | +import java.util.logging.Logger; |
| 8 | + |
| 9 | +/// Renders a JDT AST into an ES2020 module that exports a `transform(source)` function. |
| 10 | +/// |
| 11 | +/// The generated JavaScript applies the transform specification to a source document |
| 12 | +/// without interpretation overhead - all directive dispatch is resolved at render time. |
| 13 | +/// |
| 14 | +/// Usage: |
| 15 | +/// ```java |
| 16 | +/// var ast = Jdt.parseToAst(transformJson); |
| 17 | +/// String esm = JdtEsmRenderer.render(ast); |
| 18 | +/// // esm contains: export function transform(source) { ... } |
| 19 | +/// ``` |
| 20 | +public final class JdtEsmRenderer { |
| 21 | + |
| 22 | + private static final Logger LOG = Logger.getLogger(JdtEsmRenderer.class.getName()); |
| 23 | + |
| 24 | + private JdtEsmRenderer() {} |
| 25 | + |
| 26 | + /// Renders a JDT AST into an ES2020 module string. |
| 27 | + /// |
| 28 | + /// @param ast the parsed JDT AST root node |
| 29 | + /// @return a complete ES2020 module string |
| 30 | + public static String render(JdtNode ast) { |
| 31 | + final var sb = new StringBuilder(); |
| 32 | + |
| 33 | + sb.append("// Generated JDT (JSON Document Transform)\n"); |
| 34 | + sb.append("// Do not edit - generated by JdtEsmRenderer\n\n"); |
| 35 | + |
| 36 | + // Emit helper functions |
| 37 | + sb.append("function deepMerge(source, overlay) {\n"); |
| 38 | + sb.append(" if (typeof source !== 'object' || source === null || Array.isArray(source)) return overlay;\n"); |
| 39 | + sb.append(" if (typeof overlay !== 'object' || overlay === null || Array.isArray(overlay)) return overlay;\n"); |
| 40 | + sb.append(" const result = { ...source };\n"); |
| 41 | + sb.append(" for (const [k, v] of Object.entries(overlay)) {\n"); |
| 42 | + sb.append(" if (typeof result[k] === 'object' && result[k] !== null && !Array.isArray(result[k])\n"); |
| 43 | + sb.append(" && typeof v === 'object' && v !== null && !Array.isArray(v)) {\n"); |
| 44 | + sb.append(" result[k] = deepMerge(result[k], v);\n"); |
| 45 | + sb.append(" } else if (Array.isArray(result[k]) && Array.isArray(v)) {\n"); |
| 46 | + sb.append(" result[k] = [...result[k], ...v];\n"); |
| 47 | + sb.append(" } else {\n"); |
| 48 | + sb.append(" result[k] = v;\n"); |
| 49 | + sb.append(" }\n"); |
| 50 | + sb.append(" }\n"); |
| 51 | + sb.append(" return result;\n"); |
| 52 | + sb.append("}\n\n"); |
| 53 | + |
| 54 | + sb.append("export function transform(source) {\n"); |
| 55 | + emitNode(sb, ast, "source", " "); |
| 56 | + sb.append("}\n"); |
| 57 | + |
| 58 | + LOG.fine(() -> "Rendered JDT ESM module"); |
| 59 | + return sb.toString(); |
| 60 | + } |
| 61 | + |
| 62 | + private static void emitNode(StringBuilder sb, JdtNode node, String sourceVar, String indent) { |
| 63 | + switch (node) { |
| 64 | + case ReplacementNode rep -> { |
| 65 | + sb.append(indent).append("return ").append(jsonToJs(rep.value())).append(";\n"); |
| 66 | + } |
| 67 | + case MergeNode merge -> { |
| 68 | + emitMergeNode(sb, merge, sourceVar, indent); |
| 69 | + } |
| 70 | + case DirectiveNode dir -> { |
| 71 | + emitDirectiveNode(sb, dir, sourceVar, indent); |
| 72 | + } |
| 73 | + } |
| 74 | + } |
| 75 | + |
| 76 | + private static void emitMergeNode(StringBuilder sb, MergeNode merge, String sourceVar, String indent) { |
| 77 | + if (merge.children().isEmpty()) { |
| 78 | + sb.append(indent).append("return ").append(sourceVar).append(";\n"); |
| 79 | + return; |
| 80 | + } |
| 81 | + |
| 82 | + // Build overlay object from children |
| 83 | + sb.append(indent).append("const _overlay = {};\n"); |
| 84 | + for (final var entry : merge.children().entrySet()) { |
| 85 | + final var key = entry.getKey(); |
| 86 | + final var child = entry.getValue(); |
| 87 | + switch (child) { |
| 88 | + case ReplacementNode rep -> |
| 89 | + sb.append(indent).append("_overlay[").append(jsString(key)).append("] = ") |
| 90 | + .append(jsonToJs(rep.value())).append(";\n"); |
| 91 | + case MergeNode childMerge -> { |
| 92 | + final var childVar = sourceVar + "?.[" + jsString(key) + "]"; |
| 93 | + final var fnName = "_merge_" + sanitize(key); |
| 94 | + sb.append(indent).append("function ").append(fnName).append("(_s) {\n"); |
| 95 | + emitMergeNode(sb, childMerge, "_s", indent + " "); |
| 96 | + sb.append(indent).append("}\n"); |
| 97 | + sb.append(indent).append("_overlay[").append(jsString(key)).append("] = ") |
| 98 | + .append(fnName).append("(").append(childVar).append(" ?? {});\n"); |
| 99 | + } |
| 100 | + case DirectiveNode childDir -> { |
| 101 | + final var childVar = sourceVar + "?.[" + jsString(key) + "]"; |
| 102 | + final var fnName = "_apply_" + sanitize(key); |
| 103 | + sb.append(indent).append("function ").append(fnName).append("(_s) {\n"); |
| 104 | + emitDirectiveNode(sb, childDir, "_s", indent + " "); |
| 105 | + sb.append(indent).append("}\n"); |
| 106 | + sb.append(indent).append("_overlay[").append(jsString(key)).append("] = ") |
| 107 | + .append(fnName).append("(").append(childVar).append(" ?? {});\n"); |
| 108 | + } |
| 109 | + } |
| 110 | + } |
| 111 | + sb.append(indent).append("return deepMerge(").append(sourceVar).append(", _overlay);\n"); |
| 112 | + } |
| 113 | + |
| 114 | + private static void emitDirectiveNode(StringBuilder sb, DirectiveNode dir, String sourceVar, String indent) { |
| 115 | + sb.append(indent).append("let _r = ").append(sourceVar).append(";\n"); |
| 116 | + |
| 117 | + // 1. Rename |
| 118 | + if (dir.rename() != null) { |
| 119 | + emitRename(sb, dir.rename(), indent); |
| 120 | + } |
| 121 | + |
| 122 | + // 2. Remove |
| 123 | + if (dir.remove() != null) { |
| 124 | + emitRemove(sb, dir.remove(), indent); |
| 125 | + } |
| 126 | + |
| 127 | + // 3. Merge |
| 128 | + if (dir.merge() != null) { |
| 129 | + emitMerge(sb, dir.merge(), indent); |
| 130 | + } |
| 131 | + |
| 132 | + // 4. Replace |
| 133 | + if (dir.replace() != null) { |
| 134 | + emitReplace(sb, dir.replace(), indent); |
| 135 | + } |
| 136 | + |
| 137 | + // Process children as default merge |
| 138 | + for (final var entry : dir.children().entrySet()) { |
| 139 | + final var key = entry.getKey(); |
| 140 | + final var child = entry.getValue(); |
| 141 | + switch (child) { |
| 142 | + case ReplacementNode rep -> |
| 143 | + sb.append(indent).append("if (typeof _r === 'object' && _r !== null) ") |
| 144 | + .append("_r[").append(jsString(key)).append("] = ") |
| 145 | + .append(jsonToJs(rep.value())).append(";\n"); |
| 146 | + default -> |
| 147 | + sb.append(indent).append("if (typeof _r === 'object' && _r !== null && _r[") |
| 148 | + .append(jsString(key)).append("] !== undefined) _r[").append(jsString(key)) |
| 149 | + .append("] = deepMerge(_r[").append(jsString(key)).append("], ") |
| 150 | + .append(jsonToJs(child)).append(");\n"); |
| 151 | + } |
| 152 | + } |
| 153 | + |
| 154 | + sb.append(indent).append("return _r;\n"); |
| 155 | + } |
| 156 | + |
| 157 | + private static void emitRename(StringBuilder sb, JsonValue renameSpec, String indent) { |
| 158 | + sb.append(indent).append("if (typeof _r === 'object' && _r !== null) {\n"); |
| 159 | + if (renameSpec instanceof jdk.sandbox.java.util.json.JsonObject renameObj) { |
| 160 | + for (final var entry : renameObj.members().entrySet()) { |
| 161 | + if (entry.getKey().startsWith("@jdt.")) continue; |
| 162 | + if (entry.getValue() instanceof jdk.sandbox.java.util.json.JsonString newNameStr) { |
| 163 | + sb.append(indent).append(" if (").append(jsString(entry.getKey())).append(" in _r) {\n"); |
| 164 | + sb.append(indent).append(" _r[").append(jsString(newNameStr.string())) |
| 165 | + .append("] = _r[").append(jsString(entry.getKey())).append("];\n"); |
| 166 | + sb.append(indent).append(" delete _r[").append(jsString(entry.getKey())).append("];\n"); |
| 167 | + sb.append(indent).append(" }\n"); |
| 168 | + } |
| 169 | + } |
| 170 | + } |
| 171 | + sb.append(indent).append("}\n"); |
| 172 | + } |
| 173 | + |
| 174 | + private static void emitRemove(StringBuilder sb, JsonValue removeSpec, String indent) { |
| 175 | + sb.append(indent).append("if (typeof _r === 'object' && _r !== null) {\n"); |
| 176 | + if (removeSpec instanceof jdk.sandbox.java.util.json.JsonString removeStr) { |
| 177 | + sb.append(indent).append(" delete _r[").append(jsString(removeStr.string())).append("];\n"); |
| 178 | + } else if (removeSpec instanceof jdk.sandbox.java.util.json.JsonBoolean removeBool && removeBool.bool()) { |
| 179 | + sb.append(indent).append(" _r = null;\n"); |
| 180 | + } else if (removeSpec instanceof jdk.sandbox.java.util.json.JsonArray removeArr) { |
| 181 | + for (final var item : removeArr.elements()) { |
| 182 | + if (item instanceof jdk.sandbox.java.util.json.JsonString itemStr) { |
| 183 | + sb.append(indent).append(" delete _r[").append(jsString(itemStr.string())).append("];\n"); |
| 184 | + } |
| 185 | + } |
| 186 | + } |
| 187 | + sb.append(indent).append("}\n"); |
| 188 | + } |
| 189 | + |
| 190 | + private static void emitMerge(StringBuilder sb, JsonValue mergeSpec, String indent) { |
| 191 | + sb.append(indent).append("_r = deepMerge(_r, ").append(jsonToJs(mergeSpec)).append(");\n"); |
| 192 | + } |
| 193 | + |
| 194 | + private static void emitReplace(StringBuilder sb, JsonValue replaceSpec, String indent) { |
| 195 | + sb.append(indent).append("_r = ").append(jsonToJs(replaceSpec)).append(";\n"); |
| 196 | + } |
| 197 | + |
| 198 | + private static String jsonToJs(Object value) { |
| 199 | + if (value instanceof JdtNode) return "{}"; // Fallback for AST nodes in children |
| 200 | + if (value instanceof JsonValue jv) return jv.toString(); |
| 201 | + return "null"; |
| 202 | + } |
| 203 | + |
| 204 | + private static String jsString(String s) { |
| 205 | + return "\"" + s.replace("\\", "\\\\").replace("\"", "\\\"") + "\""; |
| 206 | + } |
| 207 | + |
| 208 | + private static String sanitize(String s) { |
| 209 | + return s.replaceAll("[^a-zA-Z0-9_]", "_"); |
| 210 | + } |
| 211 | +} |
0 commit comments