@fraczak/k 6.8.0 → 6.9.0

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.
@@ -14,18 +14,25 @@ import {
14
14
  formatDuration,
15
15
  resolveInputTypeHash,
16
16
  resolveInputPattern,
17
- valueForPattern
17
+ valueForPattern,
18
+ codecNames,
19
+ ensureEnveloped,
20
+ patternFromFilter
18
21
  } from "../repl.mjs";
19
- import { Value, isProduct, isVariant } from "../Value.mjs";
22
+ import { Value, isProduct, isVariant, withPattern } from "../Value.mjs";
20
23
  import { encodeLibrary } from "../object.mjs";
21
24
  import { setVfsFile, getVfsFile, getAllVfsFiles } from "./shims/fs.mjs";
22
25
  import { createOutputEntryElement } from "./tree-view.mjs";
23
26
 
24
- // Make Value and helpers globally available for custom codecs
27
+ // Make Value and helpers globally available in browser console
25
28
  if (typeof window !== "undefined") {
26
- window.Value = Value;
27
- window.isProduct = isProduct;
28
- window.isVariant = isVariant;
29
+ Object.assign(window, {
30
+ Value,
31
+ isProduct,
32
+ isVariant,
33
+ withPattern,
34
+ patternFromFilter
35
+ });
29
36
  }
30
37
 
31
38
  // Global REPL state
@@ -60,18 +67,8 @@ let helpModal;
60
67
  let dropOverlay;
61
68
  let codecsModal;
62
69
  let codecsBadgeEl;
63
- let activeCodecsTbody;
64
- let codecTypeSelect;
65
-
66
- // Input Popup Modal Elements
67
- let inputPopupModal;
68
- let inputPopupType;
69
- let inputPopupCodec;
70
- let inputPopupText;
71
- let inputPopupStatus;
72
- let inputPopupHint;
73
- let inputPopupCodecHint;
74
- let inputSamplesList;
70
+ let enabledCodecsGrid;
71
+
75
72
 
76
73
  function escapeHtml(str) {
77
74
  return String(str)
@@ -115,17 +112,7 @@ function ansiToHtml(str) {
115
112
 
116
113
  function countActiveCodecs() {
117
114
  const store = state.codecs || {};
118
- let count = 0;
119
- const seen = new Set();
120
- for (const list of Object.values(store)) {
121
- for (const c of list) {
122
- if (!seen.has(c.name)) {
123
- seen.add(c.name);
124
- count++;
125
- }
126
- }
127
- }
128
- return count;
115
+ return Object.keys(store).length;
129
116
  }
130
117
 
131
118
  function updateCodecsBadge() {
@@ -264,12 +251,6 @@ export async function executeCommand(input) {
264
251
  const trimmed = input.trim();
265
252
  if (!trimmed && !state.pendingInput) return;
266
253
 
267
- // Intercept :input without args -> open input popup
268
- if (trimmed === ":input") {
269
- openInputPopup();
270
- return;
271
- }
272
-
273
254
  // Add to history
274
255
  if (history.length === 0 || history[history.length - 1] !== input) {
275
256
  history.push(input);
@@ -324,12 +305,6 @@ export async function executeCommand(input) {
324
305
  if (codecsModal && codecsModal.classList.contains("open")) {
325
306
  renderCodecsModal();
326
307
  }
327
-
328
- // If :input <type> [codec] was executed, state.pendingInput is now set!
329
- // Open the Input Popup immediately to prompt the user for the input string!
330
- if (errors.length === 0 && state.pendingInput && trimmed.startsWith(":input ")) {
331
- openInputPopup();
332
- }
333
308
  }
334
309
 
335
310
  function adjustInputHeight() {
@@ -353,14 +328,7 @@ function handleAutocomplete() {
353
328
  }
354
329
 
355
330
  if (matches.length === 1) {
356
- hideAutocomplete();
357
- const tokenStart = lineBeforeCursor.lastIndexOf(" ") + 1;
358
- const match = matches[0];
359
- const beforeToken = text.slice(0, cursorPos - (lineBeforeCursor.length - tokenStart));
360
- const afterCursor = text.slice(cursorPos);
361
- inputEl.value = beforeToken + match + (afterCursor.startsWith(" ") ? "" : " ") + afterCursor;
362
- inputEl.selectionStart = inputEl.selectionEnd = beforeToken.length + match.length + 1;
363
- adjustInputHeight();
331
+ applyAutocomplete(matches[0]);
364
332
  return;
365
333
  }
366
334
 
@@ -402,11 +370,12 @@ function applyAutocomplete(match) {
402
370
  const text = inputEl.value;
403
371
  const cursorPos = inputEl.selectionStart;
404
372
  const lineBeforeCursor = text.slice(0, cursorPos).split("\n").pop();
405
- const tokenStart = Math.max(0, lineBeforeCursor.lastIndexOf(" ") + 1);
406
- const beforeToken = text.slice(0, cursorPos - (lineBeforeCursor.length - tokenStart));
373
+ const lineStart = cursorPos - lineBeforeCursor.length;
374
+ const beforeLine = text.slice(0, lineStart);
407
375
  const afterCursor = text.slice(cursorPos);
408
- inputEl.value = beforeToken + match + (afterCursor.startsWith(" ") ? "" : " ") + afterCursor;
409
- inputEl.selectionStart = inputEl.selectionEnd = beforeToken.length + match.length + 1;
376
+ const insertSpace = (match.endsWith("/") || afterCursor.startsWith(" ")) ? "" : " ";
377
+ inputEl.value = beforeLine + match + insertSpace + afterCursor;
378
+ inputEl.selectionStart = inputEl.selectionEnd = beforeLine.length + match.length + insertSpace.length;
410
379
  hideAutocomplete();
411
380
  adjustInputHeight();
412
381
  inputEl.focus();
@@ -566,98 +535,6 @@ function exportKlib() {
566
535
  // CODECS SUBSYSTEM UI & CONTROLS
567
536
  // ==========================================
568
537
 
569
- const CODEC_PRESETS = {
570
- yn: {
571
- name: "yn",
572
- type: "yes_no",
573
- universal: false,
574
- parse: `// Parse 'yes', 'no', 'true', 'false' into $yes_no (< {} yes, {} no >)
575
- const tag = text.trim().toLowerCase();
576
- if (tag === "yes" || tag === "true" || tag === "1") {
577
- return Value.variant("yes", Value.product({}));
578
- }
579
- if (tag === "no" || tag === "false" || tag === "0") {
580
- return Value.variant("no", Value.product({}));
581
- }
582
- throw new Error("Expected yes/no or true/false");`,
583
- print: `// Serialize yes_no value to YES / NO
584
- return value.tag === "yes" ? "YES" : "NO";`
585
- },
586
- hex: {
587
- name: "hex",
588
- type: "int",
589
- universal: false,
590
- parse: `// Parse decimal or hex (e.g. 0x2a, 42, -0x10) into $int
591
- let str = text.trim();
592
- let sign = "+";
593
- if (str.startsWith("-")) { sign = "-"; str = str.slice(1).trim(); }
594
- else if (str.startsWith("+")) { str = str.slice(1).trim(); }
595
-
596
- const num = str.startsWith("0x") || str.startsWith("0X") ? BigInt(str) : BigInt(str);
597
- if (num === 0n) sign = "+";
598
-
599
- const bitChars = num === 0n ? ["0"] : num.toString(2).split("");
600
- let v = Value.variant("_", Value.product({}));
601
- for (let i = bitChars.length - 1; i >= 0; i--) {
602
- v = Value.variant(bitChars[i], v);
603
- }
604
- return Value.variant(sign, v);`,
605
- print: `// Serialize $int to hexadecimal string (0x...)
606
- if (!isVariant(value) || (value.tag !== "+" && value.tag !== "-")) {
607
- throw new Error("Not a valid int value");
608
- }
609
- let bits = "";
610
- let node = value.value;
611
- while (isVariant(node) && node.tag !== "_") {
612
- bits += node.tag;
613
- node = node.value;
614
- }
615
- const n = bits === "" ? 0n : BigInt("0b" + bits);
616
- const hex = "0x" + n.toString(16).toUpperCase();
617
- return (value.tag === "-" && n !== 0n ? "-" : "") + hex;`
618
- },
619
- currency: {
620
- name: "currency",
621
- type: "int",
622
- universal: false,
623
- parse: `// Parse currency e.g. $42.50 or $100 into cents int
624
- const clean = text.trim().replace(/^\\$/, "").trim();
625
- const parts = clean.split(".");
626
- const dollars = BigInt(parts[0] || "0");
627
- const cents = BigInt((parts[1] || "0").padEnd(2, "0").slice(0, 2));
628
- const total = dollars * 100n + cents;
629
-
630
- const bitChars = total === 0n ? ["0"] : total.toString(2).split("");
631
- let v = Value.variant("_", Value.product({}));
632
- for (let i = bitChars.length - 1; i >= 0; i--) {
633
- v = Value.variant(bitChars[i], v);
634
- }
635
- return Value.variant("+", v);`,
636
- print: `// Format cents int as currency string ($X.XX)
637
- let bits = "";
638
- let node = value.value;
639
- while (isVariant(node) && node.tag !== "_") {
640
- bits += node.tag;
641
- node = node.value;
642
- }
643
- const n = bits === "" ? 0n : BigInt("0b" + bits);
644
- const dollars = n / 100n;
645
- const cents = (n % 100n).toString().padStart(2, "0");
646
- return "$" + dollars.toString() + "." + cents;`
647
- },
648
- blank: {
649
- name: "custom",
650
- type: "",
651
- universal: false,
652
- parse: `// Parse text string and return a K Value
653
- // Available: Value.product({ field: val }), Value.variant("tag", val)
654
- return Value.product({});`,
655
- print: `// Serialize K Value into string
656
- // value is a Value instance: inspect value.fields or value.tag & value.value
657
- return String(value);`
658
- }
659
- };
660
-
661
538
  function openCodecsModal() {
662
539
  if (!codecsModal) return;
663
540
  renderCodecsModal();
@@ -670,567 +547,105 @@ function closeCodecsModal() {
670
547
 
671
548
  function renderCodecsModal() {
672
549
  updateCodecsBadge();
550
+ const grid = enabledCodecsGrid || document.getElementById("enabled-codecs-grid");
551
+ if (!grid) return;
552
+ grid.innerHTML = "";
673
553
 
674
- // 1. Built-in codec buttons state
675
- const standardCodecs = ["int", "utf8", "json", "ieee"];
676
- for (const name of standardCodecs) {
677
- const btn = document.getElementById(`btn-toggle-codec-${name}`);
678
- const badge = document.getElementById(`codec-badge-${name}`);
679
- const isLoaded = isCodecRegistered(name);
680
- if (btn) {
681
- btn.textContent = isLoaded ? "Unload" : "Load";
682
- btn.className = isLoaded ? "btn btn-sm btn-secondary" : "btn btn-sm btn-primary";
683
- }
684
- if (badge) {
685
- badge.textContent = isLoaded ? "Active" : "Inactive";
686
- badge.className = isLoaded ? "codec-status-badge active" : "codec-status-badge";
687
- }
688
- }
689
-
690
- // 2. Populate target type dropdown
691
- if (codecTypeSelect) {
692
- const prev = codecTypeSelect.value;
693
- codecTypeSelect.innerHTML = `<option value="">-- Choose Type Alias or Code Hash --</option>`;
694
- const typeGroup = document.createElement("optgroup");
695
- typeGroup.label = "Available Type Aliases in State";
696
- const aliases = Object.entries(state.typeAliases || {}).sort(([a], [b]) => a.localeCompare(b));
697
- for (const [alias, hash] of aliases) {
698
- const opt = document.createElement("option");
699
- opt.value = alias;
700
- opt.textContent = `$ ${alias} (${hash.slice(0, 10)}...)`;
701
- typeGroup.appendChild(opt);
702
- }
703
- codecTypeSelect.appendChild(typeGroup);
704
-
705
- const universalOpt = document.createElement("option");
706
- universalOpt.value = "*";
707
- universalOpt.textContent = "* (Universal - all types)";
708
- codecTypeSelect.appendChild(universalOpt);
709
-
710
- if (prev) codecTypeSelect.value = prev;
711
- }
712
-
713
- // 3. Render active codecs table
714
- if (activeCodecsTbody) {
715
- activeCodecsTbody.innerHTML = "";
716
- const store = state.codecs || {};
717
- const entries = [];
718
- for (const [codeHash, list] of Object.entries(store)) {
719
- for (const c of list) {
720
- entries.push({ codeHash, ...c });
721
- }
722
- }
723
-
724
- if (entries.length === 0) {
725
- const tr = document.createElement("tr");
726
- tr.innerHTML = `<td colspan="5" style="text-align:center;color:var(--text-muted);padding:16px;">No codecs currently loaded. Load a built-in codec or register a custom serializer/deserializer below.</td>`;
727
- activeCodecsTbody.appendChild(tr);
728
- return;
729
- }
730
-
731
- // Invert type aliases for display
732
- const hashToAlias = {};
733
- for (const [name, hash] of Object.entries(state.typeAliases || {})) {
734
- hashToAlias[hash] = name;
735
- }
736
-
737
- for (const entry of entries) {
738
- const tr = document.createElement("tr");
739
-
740
- const nameTd = document.createElement("td");
741
- nameTd.className = "codec-cell-name";
742
- nameTd.textContent = entry.name;
743
- tr.appendChild(nameTd);
744
-
745
- const typeTd = document.createElement("td");
746
- typeTd.className = "codec-cell-type";
747
- if (entry.codeHash === "*") {
748
- typeTd.innerHTML = `<span class="badge-type universal">Universal (*)</span>`;
749
- } else {
750
- const alias = hashToAlias[entry.codeHash];
751
- typeTd.innerHTML = `<span class="badge-type">${alias ? `$${alias} ` : ""}<code title="${entry.codeHash}">${entry.codeHash.slice(0, 10)}...</code></span>`;
752
- }
753
- tr.appendChild(typeTd);
754
-
755
- const capTd = document.createElement("td");
756
- const caps = [];
757
- if (typeof entry.print === "function") caps.push(`<span class="cap-badge print">Serializer</span>`);
758
- if (typeof entry.parse === "function") caps.push(`<span class="cap-badge parse">Deserializer</span>`);
759
- capTd.innerHTML = caps.join(" ");
760
- tr.appendChild(capTd);
761
-
762
- const srcTd = document.createElement("td");
763
- srcTd.className = "codec-cell-src";
764
- srcTd.textContent = entry.source ? (entry.source === "built-in" ? "Built-in" : entry.source.split("/").pop()) : "Custom";
765
- tr.appendChild(srcTd);
766
-
767
- const actTd = document.createElement("td");
768
- const unloadBtn = document.createElement("button");
769
- unloadBtn.className = "btn btn-sm btn-danger";
770
- unloadBtn.textContent = "Unload";
771
- unloadBtn.onclick = () => {
772
- executeCommand(`:codec unload ${entry.name}`);
773
- renderCodecsModal();
774
- };
775
- actTd.appendChild(unloadBtn);
776
- tr.appendChild(actTd);
777
-
778
- activeCodecsTbody.appendChild(tr);
779
- }
780
- }
781
- }
782
-
783
- function isCodecRegistered(name) {
784
554
  const store = state.codecs || {};
785
- for (const list of Object.values(store)) {
786
- if (list.some(c => c.name === name)) return true;
787
- }
788
- return false;
789
- }
790
-
791
- function applyCodecPreset(presetKey) {
792
- const preset = CODEC_PRESETS[presetKey];
793
- if (!preset) return;
794
- const nameInput = document.getElementById("codec-custom-name");
795
- const parseArea = document.getElementById("codec-custom-parse");
796
- const printArea = document.getElementById("codec-custom-print");
797
-
798
- if (nameInput) nameInput.value = preset.name;
799
- if (parseArea) parseArea.value = preset.parse;
800
- if (printArea) printArea.value = preset.print;
801
- if (codecTypeSelect) {
802
- if (preset.type) {
803
- codecTypeSelect.value = preset.type;
804
- }
805
- }
806
- }
807
-
808
- function registerCustomCodecFromForm() {
809
- const nameInput = document.getElementById("codec-custom-name");
810
- const parseArea = document.getElementById("codec-custom-parse");
811
- const printArea = document.getElementById("codec-custom-print");
812
- const statusBox = document.getElementById("codec-studio-status");
813
-
814
- const name = nameInput?.value.trim();
815
- const rawType = codecTypeSelect?.value.trim() || "*";
816
- const parseCode = parseArea?.value.trim();
817
- const printCode = printArea?.value.trim();
818
-
819
- if (!name) {
820
- if (statusBox) statusBox.innerHTML = `<span class="line-error">Codec name is required.</span>`;
555
+ const entries = Object.values(store).sort((a, b) => a.name.localeCompare(b.name));
556
+
557
+ if (entries.length === 0) {
558
+ const empty = document.createElement("div");
559
+ empty.className = "codecs-empty-state";
560
+ empty.style.gridColumn = "1 / -1";
561
+ empty.style.padding = "32px 16px";
562
+ empty.style.textAlign = "center";
563
+ empty.style.color = "var(--text-muted)";
564
+ empty.style.background = "var(--bg-base)";
565
+ empty.style.border = "1px dashed var(--border-color)";
566
+ empty.style.borderRadius = "6px";
567
+ empty.innerHTML = `
568
+ <div style="font-size: 24px; margin-bottom: 8px;">📦</div>
569
+ <div style="font-size: 13px; font-weight: 600; color: var(--text-main); margin-bottom: 6px;">No codecs currently loaded</div>
570
+ <div style="font-size: 12px; line-height: 1.5;">Codecs are ordinary <code>.mjs</code> files.<br>Load a codec using <code>:codec load &lt;file.mjs&gt;</code> in the terminal, or select a codec file in <b>Files</b> and click <b>Load into REPL</b>.</div>
571
+ `;
572
+ grid.appendChild(empty);
821
573
  return;
822
574
  }
823
575
 
824
- try {
825
- let parseFn = null;
826
- let printFn = null;
827
-
828
- if (parseCode) {
829
- parseFn = new Function(
830
- "text", "context",
831
- `const { Value, isProduct, isVariant } = globalThis;\n${parseCode}`
832
- );
833
- }
834
-
835
- if (printCode) {
836
- printFn = new Function(
837
- "value", "context",
838
- `const { Value, isProduct, isVariant } = globalThis;\n${printCode}`
839
- );
840
- }
841
-
842
- let codeHash = "*";
843
- if (rawType !== "*") {
844
- codeHash = resolveInputTypeHash(state, rawType);
845
- }
846
-
847
- registerCodec(state, {
848
- name,
849
- codes: [codeHash],
850
- universal: codeHash === "*",
851
- parse: parseFn,
852
- print: printFn
853
- }, "<custom-studio>");
854
-
855
- if (statusBox) {
856
- statusBox.innerHTML = `<span class="line-output">✓ Successfully registered codec <b>${name}</b> for ${codeHash === "*" ? "all types" : codeHash}!</span>`;
857
- }
858
-
859
- appendSystemMessage(`⚙ Registered custom codec <b>${name}</b> (serializer/deserializer) for ${codeHash === "*" ? "all types" : codeHash}.`);
860
- renderCodecsModal();
861
- } catch (err) {
862
- if (statusBox) {
863
- statusBox.innerHTML = `<span class="line-error">Error: ${escapeHtml(err.message)}</span>`;
864
- }
865
- }
866
- }
867
-
868
- function testCustomParse() {
869
- const parseArea = document.getElementById("codec-custom-parse");
870
- const testInput = document.getElementById("codec-test-input");
871
- const statusBox = document.getElementById("codec-studio-status");
872
-
873
- try {
874
- const parseFn = new Function(
875
- "text", "context",
876
- `const { Value, isProduct, isVariant } = globalThis;\n${parseArea.value}`
877
- );
878
- const result = parseFn(testInput.value, { Value, isProduct, isVariant, state });
879
- if (statusBox) {
880
- statusBox.innerHTML = `<span class="line-output"><b>Parse Result:</b> ${escapeHtml(JSON.stringify(result, null, 2))}</span>`;
881
- }
882
- } catch (err) {
883
- if (statusBox) {
884
- statusBox.innerHTML = `<span class="line-error">Parse Error: ${escapeHtml(err.message)}</span>`;
885
- }
886
- }
887
- }
888
-
889
- function testCustomPrint() {
890
- const printArea = document.getElementById("codec-custom-print");
891
- const statusBox = document.getElementById("codec-studio-status");
892
-
893
- try {
894
- const printFn = new Function(
895
- "value", "context",
896
- `const { Value, isProduct, isVariant } = globalThis;\n${printArea.value}`
897
- );
898
- const result = printFn(state.value, { Value, isProduct, isVariant, state });
899
- if (statusBox) {
900
- statusBox.innerHTML = `<span class="line-output"><b>Print Output:</b> ${escapeHtml(String(result))}</span>`;
901
- }
902
- } catch (err) {
903
- if (statusBox) {
904
- statusBox.innerHTML = `<span class="line-error">Print Error: ${escapeHtml(err.message)}</span>`;
905
- }
906
- }
907
- }
908
-
909
- function saveCustomCodecToVfs() {
910
- const nameInput = document.getElementById("codec-custom-name");
911
- const parseArea = document.getElementById("codec-custom-parse");
912
- const printArea = document.getElementById("codec-custom-print");
913
- const statusBox = document.getElementById("codec-studio-status");
914
-
915
- const name = nameInput?.value.trim() || "custom";
916
- const rawType = codecTypeSelect?.value.trim() || "*";
917
- const codeHash = rawType === "*" ? "*" : state.typeAliases[rawType] || rawType;
918
- const fileName = `codecs/${name}-codec.mjs`;
919
-
920
- const fileContent = `// Custom K Codec: ${name}
921
- import { Value, isProduct, isVariant } from "../Value.mjs";
922
-
923
- export const name = ${JSON.stringify(name)};
924
- export const codes = [${JSON.stringify(codeHash)}];
925
- export const universal = ${codeHash === "*"};
926
-
927
- export function parse(text, context) {
928
- ${parseArea?.value || ""}
929
- }
930
-
931
- export function print(value, context) {
932
- ${printArea?.value || ""}
933
- }
934
- `;
935
-
936
- setVfsFile(fileName, fileContent);
937
- if (statusBox) {
938
- statusBox.innerHTML = `<span class="line-output">💾 Saved codec file to VFS as <b>${fileName}</b></span>`;
939
- }
940
- appendSystemMessage(`💾 Saved codec file to VFS: <b>${fileName}</b>. You can reload it anytime via <code>:codec load ${fileName}</code>.`);
941
- }
942
-
943
- // ==========================================
944
- // CODEC INPUT POPUP SUBSYSTEM (:input)
945
- // ==========================================
946
-
947
- const CODEC_SAMPLES = {
948
- int: ["0", "10", "42", "-15", "1000000"],
949
- utf8: ["hello", "hello world", "k repl", "abc"],
950
- json: ['{"x":12,"n":"Woj"}', '{"name":"Alice"}', 'true', '[1, 2, 3]'],
951
- ieee: ["0.0", "3.14159", "-2.718", "1e6"],
952
- yn: ["yes", "no", "true", "false"],
953
- hex: ["0x2A", "0xFF", "0x1000", "-0x10"],
954
- currency: ["$10.00", "$42.50", "$99.99"]
955
- };
956
-
957
- export function openInputPopup(preselectedType = null, preselectedCodec = null) {
958
- if (!inputPopupModal) return;
959
-
960
- populateInputPopupDropdowns(preselectedType, preselectedCodec);
961
-
962
- if (inputPopupText) {
963
- inputPopupText.value = "";
964
- }
965
- updateLiveValidation();
966
-
967
- inputPopupModal.classList.add("open");
968
- setTimeout(() => {
969
- if (inputPopupText) inputPopupText.focus();
970
- }, 50);
971
- }
972
-
973
- export function closeInputPopup() {
974
- if (inputPopupModal) {
975
- inputPopupModal.classList.remove("open");
976
- }
977
- if (inputEl) inputEl.focus();
978
- }
979
-
980
- export function cancelInputPopup() {
981
- if (state.pendingInput) {
982
- state.pendingInput = null;
983
- updatePrompt();
984
- appendSystemMessage("Input cancelled.");
985
- }
986
- closeInputPopup();
987
- }
988
-
989
- function renderInputSamples(codecName) {
990
- if (!inputSamplesList) return;
991
- inputSamplesList.innerHTML = "";
992
- const samples = CODEC_SAMPLES[codecName] || ["()"];
993
- for (const s of samples) {
994
- const btn = document.createElement("span");
995
- btn.className = "sample-pill";
996
- btn.textContent = s;
997
- btn.title = `Click to insert sample "${s}"`;
998
- btn.onclick = () => {
999
- if (inputPopupText) {
1000
- inputPopupText.value = s;
1001
- if (codecName === "json" && s === '{"x":12,"n":"Woj"}' && (!inputPopupType?.value || inputPopupType.value === "*")) {
1002
- inputPopupType.value = "{float64 x, string n}";
1003
- updateInputPopupCodecs();
1004
- }
1005
- inputPopupText.focus();
1006
- updateLiveValidation();
1007
- }
576
+ for (const entry of entries) {
577
+ const card = document.createElement("div");
578
+ card.className = "codec-card active";
579
+ card.id = `card-codec-${entry.name.replace(/[^a-zA-Z0-9_-]/g, "_")}`;
580
+
581
+ const titleEl = document.createElement("div");
582
+ titleEl.className = "codec-card-title";
583
+ titleEl.innerHTML = `
584
+ <span class="codec-icon">📦</span>
585
+ <span class="codec-name">${escapeHtml(entry.name)}</span>
586
+ <span class="codec-status-badge active">Active</span>
587
+ `;
588
+ card.appendChild(titleEl);
589
+
590
+ let targetDesc = "Self-describing";
591
+ if (entry.name === "int.mjs") {
592
+ targetDesc = "Pattern Family: $ int & lists";
593
+ } else if (entry.name === "utf8.mjs") {
594
+ targetDesc = "$ string";
595
+ } else if (entry.name === "ieee.mjs") {
596
+ targetDesc = "$ float64";
597
+ } else if (entry.name === "unit.mjs") {
598
+ targetDesc = "{}";
599
+ } else if (entry.name === "json.mjs") {
600
+ targetDesc = "Self-describing JSON";
601
+ } else if (Array.isArray(entry.patterns) && entry.patterns.length > 0) {
602
+ targetDesc = `Pattern Family (${entry.patterns.length})`;
603
+ } else if (entry.pattern) {
604
+ targetDesc = "Pattern Recipe";
605
+ }
606
+
607
+ const typeDiv = document.createElement("div");
608
+ typeDiv.className = "codec-types";
609
+ typeDiv.innerHTML = `Target: <span class="badge-type">${escapeHtml(targetDesc)}</span>`;
610
+ card.appendChild(typeDiv);
611
+
612
+ const descDiv = document.createElement("div");
613
+ descDiv.className = "codec-desc";
614
+ const src = entry.source || entry.name;
615
+ descDiv.innerHTML = `Source: <code>${escapeHtml(src)}</code>`;
616
+ card.appendChild(descDiv);
617
+
618
+ const footer = document.createElement("div");
619
+ footer.className = "codec-card-footer";
620
+
621
+ const capsDiv = document.createElement("div");
622
+ capsDiv.className = "codec-caps";
623
+ const caps = [];
624
+ if (typeof entry.print === "function") caps.push('<span class="cap-badge print">Serializer</span>');
625
+ if (typeof entry.parse === "function") caps.push('<span class="cap-badge parse">Deserializer</span>');
626
+ capsDiv.innerHTML = caps.join(" ");
627
+ footer.appendChild(capsDiv);
628
+
629
+ const unloadBtn = document.createElement("button");
630
+ unloadBtn.className = "btn btn-sm btn-danger";
631
+ unloadBtn.textContent = "Unload";
632
+ unloadBtn.onclick = async () => {
633
+ await executeCommand(`:codec unload ${entry.name}`);
634
+ renderCodecsModal();
1008
635
  };
1009
- inputSamplesList.appendChild(btn);
1010
- }
1011
- }
636
+ footer.appendChild(unloadBtn);
1012
637
 
1013
- function updateInputPopupHint() {
1014
- if (!inputPopupHint) return;
1015
- const rawPattern = inputPopupType ? inputPopupType.value.trim() : "(...)";
1016
- const codecName = inputPopupCodec?.value || state.pendingInput?.codecName;
1017
- if (state.pendingInput) {
1018
- const displayPattern = rawPattern || state.pendingInput.rawExpr || "(...)";
1019
- inputPopupHint.innerHTML = `Enter input value for <b>${escapeHtml(displayPattern)}</b> using codec <b>${escapeHtml(codecName || "default")}</b>:`;
1020
- } else {
1021
- inputPopupHint.innerHTML = `Choose target pattern &amp; deserializer, then enter input string:`;
1022
- }
1023
-
1024
- if (inputPopupCodecHint) {
1025
- if (codecName === "json") {
1026
- inputPopupCodecHint.textContent = "Parameterized codec: deserializes JSON into the specified Target Pattern.";
1027
- } else {
1028
- inputPopupCodecHint.textContent = "";
1029
- }
638
+ card.appendChild(footer);
639
+ grid.appendChild(card);
1030
640
  }
1031
641
  }
1032
642
 
1033
- function updateInputPopupCodecs() {
1034
- if (!inputPopupCodec) return;
1035
- const rawPattern = inputPopupType ? inputPopupType.value.trim() : "(...)";
1036
- let codeHash = null;
1037
- if (rawPattern && rawPattern !== "*" && rawPattern !== "(...)") {
1038
- try {
1039
- const resolved = resolveInputPattern(state, rawPattern);
1040
- codeHash = resolved.codeHash;
1041
- } catch {
1042
- codeHash = null;
1043
- }
1044
- }
1045
- const prev = inputPopupCodec.value;
1046
-
1047
- inputPopupCodec.innerHTML = "";
1048
- const matchingCodecs = [];
643
+ function isCodecRegistered(name) {
1049
644
  const store = state.codecs || {};
1050
-
1051
- const candidates = [
1052
- ...(codeHash ? (store[codeHash] || []) : []),
1053
- ...(store["*"] || [])
1054
- ];
1055
-
1056
- const seen = new Set();
1057
- for (const c of candidates) {
1058
- if (typeof c.parse === "function" && !seen.has(c.name)) {
1059
- seen.add(c.name);
1060
- matchingCodecs.push(c.name);
1061
- }
1062
- }
1063
-
1064
- const bareName = rawPattern.replace(/^(\?|\$)\s*/, "").trim();
1065
- if (!seen.has("json")) {
1066
- matchingCodecs.push("json");
1067
- seen.add("json");
1068
- }
1069
- if ((bareName === "int" || (codeHash && codeHash === state.typeAliases?.int)) && !seen.has("int")) {
1070
- matchingCodecs.push("int");
1071
- seen.add("int");
1072
- }
1073
- if ((bareName === "string" || bareName === "utf8" || (codeHash && (codeHash === state.typeAliases?.string || codeHash === state.typeAliases?.utf8))) && !seen.has("utf8")) {
1074
- matchingCodecs.push("utf8");
1075
- seen.add("utf8");
1076
- }
1077
- if ((bareName === "float64" || bareName === "ieee" || (codeHash && codeHash === state.typeAliases?.float64)) && !seen.has("ieee")) {
1078
- matchingCodecs.push("ieee");
1079
- seen.add("ieee");
1080
- }
1081
- if ((bareName === "yes_no" || bareName === "bool" || (codeHash && codeHash === state.typeAliases?.yes_no)) && !seen.has("yn")) {
1082
- matchingCodecs.push("yn");
1083
- seen.add("yn");
1084
- }
1085
-
1086
- for (const name of matchingCodecs) {
1087
- const opt = document.createElement("option");
1088
- opt.value = name;
1089
- opt.textContent = name;
1090
- inputPopupCodec.appendChild(opt);
1091
- }
1092
-
1093
- if (prev && matchingCodecs.includes(prev)) {
1094
- inputPopupCodec.value = prev;
1095
- } else if (matchingCodecs.length > 0) {
1096
- inputPopupCodec.value = matchingCodecs[0];
1097
- }
1098
-
1099
- renderInputSamples(inputPopupCodec.value);
1100
- updateInputPopupHint();
1101
- updateLiveValidation();
1102
- }
1103
-
1104
- function populateInputPopupDropdowns(preselectedPattern = null, preselectedCodec = null) {
1105
- if (!inputPopupType) return;
1106
-
1107
- const datalist = document.getElementById("input-popup-type-list");
1108
- if (datalist) {
1109
- datalist.innerHTML = "";
1110
- const aliases = Object.entries(state.typeAliases || {}).sort(([a], [b]) => a.localeCompare(b));
1111
- for (const [alias, hash] of aliases) {
1112
- const opt = document.createElement("option");
1113
- opt.value = `$ ${alias}`;
1114
- opt.textContent = `$ ${alias} (${hash.slice(0, 10)}...)`;
1115
- datalist.appendChild(opt);
1116
- }
1117
- const universalOpt = document.createElement("option");
1118
- universalOpt.value = "(...)";
1119
- universalOpt.textContent = "(...) (Any document)";
1120
- datalist.appendChild(universalOpt);
1121
- }
1122
-
1123
- let prevPattern = preselectedPattern;
1124
- if (!prevPattern && state.pendingInput) {
1125
- prevPattern = state.pendingInput.rawExpr;
1126
- if (!prevPattern && state.pendingInput.codeHash) {
1127
- const foundAlias = Object.entries(state.typeAliases || {}).find(([, h]) => h === state.pendingInput.codeHash);
1128
- prevPattern = foundAlias ? `$ ${foundAlias[0]}` : state.pendingInput.codeHash;
1129
- }
1130
- }
1131
- if (!prevPattern) {
1132
- prevPattern = inputPopupType.value || "(...)";
1133
- }
1134
-
1135
- inputPopupType.value = prevPattern;
1136
- updateInputPopupCodecs();
1137
-
1138
- const targetCodec = preselectedCodec || state.pendingInput?.codecName;
1139
- if (targetCodec && inputPopupCodec) {
1140
- inputPopupCodec.value = targetCodec;
1141
- renderInputSamples(targetCodec);
1142
- }
1143
- }
1144
-
1145
- function updateLiveValidation() {
1146
- if (!inputPopupStatus) return;
1147
- const text = inputPopupText ? inputPopupText.value : "";
1148
- if (!text) {
1149
- inputPopupStatus.innerHTML = `<span style="color:var(--text-muted);font-size:11px;">Ready for input.</span>`;
1150
- return;
1151
- }
1152
-
1153
- const rawPattern = inputPopupType ? inputPopupType.value.trim() : "(...)";
1154
- const codecName = inputPopupCodec?.value;
1155
- if (!rawPattern && !codecName) {
1156
- inputPopupStatus.innerHTML = `<span class="line-warning" style="margin:0;padding:2px 6px;">Specify a target pattern or codec first.</span>`;
1157
- return;
1158
- }
1159
-
1160
- let pattern = null;
1161
- let codeHash = null;
1162
- if (rawPattern && rawPattern !== "*" && rawPattern !== "(...)") {
1163
- try {
1164
- const resolved = resolveInputPattern(state, rawPattern);
1165
- pattern = resolved.pattern;
1166
- codeHash = resolved.codeHash;
1167
- } catch (err) {
1168
- inputPopupStatus.innerHTML = `<span class="line-warning" style="margin:0;padding:2px 6px;">⚠ Pattern Error: ${escapeHtml(err.message || String(err))}</span>`;
1169
- return;
1170
- }
1171
- }
1172
-
1173
- try {
1174
- const codec = resolveCodec(state, codeHash, codecName || null, "parse");
1175
- const resolvedPattern = pattern || (codeHash && codeHash !== "*" ? codeHashToPattern(codeHash, (h) => state.codes?.[h]) : null);
1176
- const parsed = codec.parse(text, {
1177
- codeHash,
1178
- pattern: resolvedPattern,
1179
- state,
1180
- Value,
1181
- isProduct,
1182
- isVariant
1183
- });
1184
- const validated = valueForPattern(parsed, resolvedPattern, text, codec.name);
1185
- const repr = validated instanceof Value ? validated.toJSON() : (typeof validated === "object" ? JSON.stringify(validated) : String(validated));
1186
- inputPopupStatus.innerHTML = `<span class="line-output" style="margin:0;padding:2px 6px;">✓ Valid: <code>${escapeHtml(String(repr))}</code></span>`;
1187
- } catch (err) {
1188
- inputPopupStatus.innerHTML = `<span class="line-warning" style="margin:0;padding:2px 6px;">⚠ ${escapeHtml(err.message || String(err))}</span>`;
1189
- }
645
+ return Boolean(store[name]);
1190
646
  }
1191
647
 
1192
- export async function submitInputPopup() {
1193
- const text = inputPopupText?.value ?? "";
1194
- const rawPattern = inputPopupType ? inputPopupType.value.trim() : "(...)";
1195
- const codecName = inputPopupCodec?.value;
1196
648
 
1197
- if (!rawPattern && !codecName) {
1198
- if (inputPopupStatus) inputPopupStatus.innerHTML = `<span class="line-error">Please specify a target pattern or codec.</span>`;
1199
- return;
1200
- }
1201
-
1202
- let pattern = null;
1203
- let codeHash = null;
1204
- let rawExpr = rawPattern || "(...)";
1205
- if (rawPattern && rawPattern !== "*" && rawPattern !== "(...)") {
1206
- try {
1207
- const resolved = resolveInputPattern(state, rawPattern);
1208
- pattern = resolved.pattern;
1209
- codeHash = resolved.codeHash;
1210
- rawExpr = rawPattern;
1211
- } catch (err) {
1212
- if (inputPopupStatus) inputPopupStatus.innerHTML = `<span class="line-error">Pattern Error: ${escapeHtml(err.message || String(err))}</span>`;
1213
- return;
1214
- }
1215
- }
1216
-
1217
- try {
1218
- const codec = resolveCodec(state, codeHash, codecName || null, "parse");
1219
- state.pendingInput = {
1220
- pattern,
1221
- codeHash,
1222
- codecName: codec.name,
1223
- promptName: codec.name,
1224
- rawExpr: rawExpr || codec.name
1225
- };
1226
- } catch (err) {
1227
- if (inputPopupStatus) inputPopupStatus.innerHTML = `<span class="line-error">${escapeHtml(err.message || String(err))}</span>`;
1228
- return;
1229
- }
1230
-
1231
- closeInputPopup();
1232
- await executeCommand(text);
1233
- }
1234
649
 
1235
650
  // Initialization on DOMContentLoaded
1236
651
  export function initRepl() {
@@ -1248,17 +663,8 @@ export function initRepl() {
1248
663
  dropOverlay = document.getElementById("drop-overlay");
1249
664
  codecsModal = document.getElementById("codecs-modal");
1250
665
  codecsBadgeEl = document.getElementById("codecs-count-badge");
1251
- activeCodecsTbody = document.getElementById("active-codecs-tbody");
1252
- codecTypeSelect = document.getElementById("codec-custom-type");
1253
-
1254
- inputPopupModal = document.getElementById("input-popup-modal");
1255
- inputPopupType = document.getElementById("input-popup-type");
1256
- inputPopupCodec = document.getElementById("input-popup-codec");
1257
- inputPopupText = document.getElementById("input-popup-text");
1258
- inputPopupStatus = document.getElementById("input-popup-status");
1259
- inputPopupHint = document.getElementById("input-popup-hint");
1260
- inputPopupCodecHint = document.getElementById("input-popup-codec-hint");
1261
- inputSamplesList = document.getElementById("input-samples-list");
666
+ enabledCodecsGrid = document.getElementById("enabled-codecs-grid");
667
+
1262
668
 
1263
669
  updatePrompt();
1264
670
 
@@ -1459,78 +865,7 @@ export function initRepl() {
1459
865
  const closeCodecsBtn = document.getElementById("btn-close-codecs");
1460
866
  if (closeCodecsBtn) closeCodecsBtn.onclick = closeCodecsModal;
1461
867
 
1462
- // Toggle built-in codecs buttons
1463
- ["int", "utf8", "json", "ieee"].forEach(name => {
1464
- const btn = document.getElementById(`btn-toggle-codec-${name}`);
1465
- if (btn) {
1466
- btn.onclick = () => {
1467
- if (isCodecRegistered(name)) {
1468
- executeCommand(`:codec unload ${name}`);
1469
- } else {
1470
- executeCommand(`:codec load ${name}`);
1471
- }
1472
- renderCodecsModal();
1473
- };
1474
- }
1475
- });
1476
-
1477
- // Codec Studio controls
1478
- const presetSelect = document.getElementById("codec-preset-select");
1479
- if (presetSelect) {
1480
- presetSelect.addEventListener("change", () => {
1481
- if (presetSelect.value) applyCodecPreset(presetSelect.value);
1482
- });
1483
- }
1484
-
1485
- const btnRegisterCodec = document.getElementById("btn-register-codec");
1486
- if (btnRegisterCodec) btnRegisterCodec.onclick = registerCustomCodecFromForm;
1487
-
1488
- const btnTestParse = document.getElementById("btn-test-parse");
1489
- if (btnTestParse) btnTestParse.onclick = testCustomParse;
1490
-
1491
- const btnTestPrint = document.getElementById("btn-test-print");
1492
- if (btnTestPrint) btnTestPrint.onclick = testCustomPrint;
1493
-
1494
- const btnSaveCodecVfs = document.getElementById("btn-save-codec-vfs");
1495
- if (btnSaveCodecVfs) btnSaveCodecVfs.onclick = saveCustomCodecToVfs;
1496
-
1497
- // Codec Input Popup controls
1498
- const btnInputNavbar = document.getElementById("btn-input-popup");
1499
- if (btnInputNavbar) btnInputNavbar.onclick = () => openInputPopup();
1500
-
1501
- const btnCloseInput = document.getElementById("btn-close-input-popup");
1502
- if (btnCloseInput) btnCloseInput.onclick = cancelInputPopup;
1503
868
 
1504
- const btnCancelInput = document.getElementById("btn-cancel-input");
1505
- if (btnCancelInput) btnCancelInput.onclick = cancelInputPopup;
1506
-
1507
- const btnSubmitInput = document.getElementById("btn-submit-input");
1508
- if (btnSubmitInput) btnSubmitInput.onclick = submitInputPopup;
1509
-
1510
- if (inputPopupType) {
1511
- inputPopupType.addEventListener("change", updateInputPopupCodecs);
1512
- inputPopupType.addEventListener("input", updateInputPopupCodecs);
1513
- }
1514
-
1515
- if (inputPopupCodec) {
1516
- inputPopupCodec.addEventListener("change", () => {
1517
- renderInputSamples(inputPopupCodec.value);
1518
- updateLiveValidation();
1519
- });
1520
- }
1521
-
1522
- if (inputPopupText) {
1523
- inputPopupText.addEventListener("input", updateLiveValidation);
1524
- inputPopupText.addEventListener("keydown", (e) => {
1525
- if (e.key === "Enter" && (e.ctrlKey || e.metaKey || !e.shiftKey)) {
1526
- e.preventDefault();
1527
- submitInputPopup();
1528
- } else if (e.key === "Escape") {
1529
- e.preventDefault();
1530
- cancelInputPopup();
1531
- }
1532
- });
1533
- }
1534
869
 
1535
870
  // Export .klib button
1536
871
  const exportBtn = document.getElementById("btn-export");
@@ -1589,7 +924,6 @@ export function initRepl() {
1589
924
  if (e.target === vfsModal) closeVfsModal();
1590
925
  if (e.target === helpModal) helpModal.classList.remove("open");
1591
926
  if (e.target === codecsModal) closeCodecsModal();
1592
- if (e.target === inputPopupModal) cancelInputPopup();
1593
927
  });
1594
928
 
1595
929
  // Focus input when clicking anywhere in the input bar
@@ -1615,9 +949,6 @@ export function initRepl() {
1615
949
  }
1616
950
  });
1617
951
 
1618
- // Apply initial yn preset to custom form
1619
- applyCodecPreset("yn");
1620
-
1621
952
  inputEl.focus();
1622
953
  }
1623
954
 
@@ -1638,10 +969,6 @@ if (typeof window !== "undefined") {
1638
969
  setVfsFile,
1639
970
  getAllVfsFiles,
1640
971
  openCodecsModal,
1641
- openInputPopup,
1642
- closeInputPopup,
1643
- submitInputPopup,
1644
- cancelInputPopup,
1645
972
  registerCodec,
1646
973
  unregisterCodec
1647
974
  };