eyelang 1.1.13 → 1.1.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eyelang",
3
- "version": "1.1.13",
3
+ "version": "1.1.14",
4
4
  "description": "A small Prolog-syntax-subset logic programming language for rules, goals, answers, and proofs.",
5
5
  "type": "module",
6
6
  "main": "./index.js",
package/playground.html CHANGED
@@ -408,6 +408,7 @@
408
408
  <button id="copy-source" type="button">Copy source</button>
409
409
  <button id="copy-output" type="button">Copy output</button>
410
410
  <button id="share" type="button">Copy share link</button>
411
+ <button id="create-gist" type="button">Create Gist share</button>
411
412
  </div>
412
413
  </section>
413
414
 
@@ -566,6 +567,8 @@
566
567
  answer(ok) :- eq(ok, ok).
567
568
  `;
568
569
  const HIGHLIGHT_LIMIT = 200000;
570
+ const MAX_SHARE_URL_LENGTH = 1900;
571
+ const GIST_STATE_FILENAME = 'eyelang-playground-state.json';
569
572
  const KEYWORDS = new Set(['materialize', 'memoize']);
570
573
  const BUILTINS = new Set([
571
574
  "abs",
@@ -671,12 +674,17 @@ answer(ok) :- eq(ok, ok).
671
674
  let activeWorkerUrl = null;
672
675
  let renderToken = 0;
673
676
  let syntaxErrorLine = null;
677
+ let sourceReference = { kind: 'custom' };
678
+ let sourceDirty = false;
679
+ let backgroundReference = null;
674
680
 
675
681
  populateExamples(EXAMPLES);
676
- restoreFromHash() || loadExample('ancestor');
682
+ await restoreFromHashOrLoadDefault();
677
683
  loadVersion();
678
684
 
679
685
  source.addEventListener('input', () => {
686
+ sourceDirty = true;
687
+ sourceReference = { kind: 'custom' };
680
688
  clearSyntaxError();
681
689
  render();
682
690
  });
@@ -690,6 +698,7 @@ answer(ok) :- eq(ok, ok).
690
698
  document.querySelector('#copy-source').addEventListener('click', () => copyText(source.value, 'Source copied.'));
691
699
  document.querySelector('#copy-output').addEventListener('click', () => copyText(output.textContent, 'Output copied.'));
692
700
  document.querySelector('#share').addEventListener('click', copyShareLink);
701
+ document.querySelector('#create-gist').addEventListener('click', createGistShare);
693
702
 
694
703
  function populateExamples(names) {
695
704
  const current = exampleSelect.value;
@@ -716,7 +725,7 @@ answer(ok) :- eq(ok, ok).
716
725
  const exampleUrl = new URL(`./examples/${name}.pl`, location.href);
717
726
  const response = await fetch(exampleUrl, { cache: 'no-store' });
718
727
  if (!response.ok) throw new Error(`${response.status} ${response.statusText}`);
719
- setSource(await response.text(), `examples/${name}.pl`);
728
+ setSource(await response.text(), `examples/${name}.pl`, { kind: 'example', name });
720
729
  setStatus(`Loaded examples/${name}.pl.`);
721
730
  } catch (error) {
722
731
  setSource(FALLBACK_SOURCE, 'fallback program');
@@ -735,10 +744,11 @@ answer(ok) :- eq(ok, ok).
735
744
  if (loadBackground.checked) {
736
745
  backgroundSource = text;
737
746
  backgroundName = url;
747
+ backgroundReference = { kind: 'url', url };
738
748
  updateBackgroundStatus();
739
749
  setStatus(`Loaded background knowledge from ${url}.`);
740
750
  } else {
741
- setSource(text, url);
751
+ setSource(text, url, { kind: 'url', url });
742
752
  setStatus(`Loaded ${url}.`);
743
753
  }
744
754
  } catch (error) {
@@ -749,6 +759,7 @@ answer(ok) :- eq(ok, ok).
749
759
  function clearBackground() {
750
760
  backgroundSource = '';
751
761
  backgroundName = '';
762
+ backgroundReference = null;
752
763
  updateBackgroundStatus();
753
764
  setStatus('Background knowledge cleared.');
754
765
  }
@@ -759,9 +770,11 @@ answer(ok) :- eq(ok, ok).
759
770
  : 'No background knowledge loaded.';
760
771
  }
761
772
 
762
- function setSource(text, name) {
773
+ function setSource(text, name, reference = { kind: 'custom' }) {
763
774
  source.value = text;
764
775
  sourceName.textContent = name;
776
+ sourceReference = reference;
777
+ sourceDirty = false;
765
778
  clearSyntaxError();
766
779
  render();
767
780
  }
@@ -1001,39 +1014,176 @@ answer(ok) :- eq(ok, ok).
1001
1014
  }
1002
1015
 
1003
1016
  async function copyShareLink() {
1004
- const payload = JSON.stringify({
1017
+ const link = buildShareLink();
1018
+ if (link == null) {
1019
+ setStatus('This program is too large for a reliable URL. Use “Create Gist share” instead.', true);
1020
+ return;
1021
+ }
1022
+ await copyText(link, 'Share link copied.');
1023
+ }
1024
+
1025
+ function buildShareLink() {
1026
+ const referenced = buildReferenceShareLink();
1027
+ if (referenced && referenced.length <= MAX_SHARE_URL_LENGTH) return referenced;
1028
+ const embedded = `${basePlaygroundUrl()}#state=${encodeState(currentShareState())}`;
1029
+ return embedded.length <= MAX_SHARE_URL_LENGTH ? embedded : null;
1030
+ }
1031
+
1032
+ function buildReferenceShareLink() {
1033
+ const params = new URLSearchParams();
1034
+ if (proof.checked) params.set('proof', '1');
1035
+ if (stats.checked) params.set('stats', '1');
1036
+ if (backgroundSource.trim()) {
1037
+ if (backgroundReference?.kind !== 'url') return null;
1038
+ params.set('background-url', backgroundReference.url);
1039
+ }
1040
+ if (!sourceDirty && sourceReference.kind === 'example') params.set('example', sourceReference.name);
1041
+ else if (!sourceDirty && sourceReference.kind === 'url') params.set('url', sourceReference.url);
1042
+ else return null;
1043
+ return `${basePlaygroundUrl()}#${params.toString()}`;
1044
+ }
1045
+
1046
+ async function createGistShare() {
1047
+ const stateText = JSON.stringify(currentShareState(), null, 2);
1048
+ const token = prompt('Optional GitHub token with gist scope. It is only sent to api.github.com and is not stored. Leave blank to copy Gist-ready state instead.');
1049
+ if (token === null) return;
1050
+ if (!token.trim()) {
1051
+ await copyText(stateText, `Gist-ready state copied. Create a Gist file named ${GIST_STATE_FILENAME}, paste it, then share its raw URL with #state-url=...`);
1052
+ window.open('https://gist.github.com/', '_blank', 'noopener');
1053
+ return;
1054
+ }
1055
+
1056
+ try {
1057
+ setStatus('Creating GitHub Gist…');
1058
+ const files = {
1059
+ [GIST_STATE_FILENAME]: { content: stateText },
1060
+ 'program.pl': { content: source.value },
1061
+ };
1062
+ if (backgroundSource.trim()) files['background.pl'] = { content: backgroundSource };
1063
+ const response = await fetch('https://api.github.com/gists', {
1064
+ method: 'POST',
1065
+ headers: {
1066
+ Accept: 'application/vnd.github+json',
1067
+ Authorization: `Bearer ${token.trim()}`,
1068
+ 'Content-Type': 'application/json',
1069
+ },
1070
+ body: JSON.stringify({
1071
+ description: 'Eyelang playground share',
1072
+ public: false,
1073
+ files,
1074
+ }),
1075
+ });
1076
+ const gist = await response.json().catch(() => ({}));
1077
+ if (!response.ok) throw new Error(gist.message || `${response.status} ${response.statusText}`);
1078
+ const rawUrl = gist.files?.[GIST_STATE_FILENAME]?.raw_url;
1079
+ if (!rawUrl) throw new Error('Gist response did not include a raw state URL.');
1080
+ await copyText(`${basePlaygroundUrl()}#state-url=${encodeURIComponent(rawUrl)}`, 'Gist share link copied.');
1081
+ } catch (error) {
1082
+ await copyText(stateText, `Could not create Gist: ${formatError(error)}. Gist-ready state copied instead.`);
1083
+ }
1084
+ }
1085
+
1086
+ function currentShareState() {
1087
+ return {
1005
1088
  source: source.value,
1006
1089
  proof: proof.checked,
1007
1090
  stats: stats.checked,
1008
1091
  backgroundSource,
1009
1092
  backgroundName,
1010
- });
1011
- const data = new TextEncoder().encode(payload);
1093
+ };
1094
+ }
1095
+
1096
+ function encodeState(payload) {
1097
+ const data = new TextEncoder().encode(JSON.stringify(payload));
1012
1098
  let binary = '';
1013
1099
  for (const byte of data) binary += String.fromCharCode(byte);
1014
- const encoded = btoa(binary).replaceAll('+', '-').replaceAll('/', '_').replace(/=+$/, '');
1015
- await copyText(`${location.href.split('#')[0]}#state=${encoded}`, 'Share link copied.');
1100
+ return btoa(binary).replaceAll('+', '-').replaceAll('/', '_').replace(/=+$/, '');
1101
+ }
1102
+
1103
+ function decodeState(encoded) {
1104
+ const base64 = encoded.replaceAll('-', '+').replaceAll('_', '/');
1105
+ const padded = base64 + '='.repeat((4 - base64.length % 4) % 4);
1106
+ const bytes = Uint8Array.from(atob(padded), (ch) => ch.charCodeAt(0));
1107
+ return JSON.parse(new TextDecoder().decode(bytes));
1108
+ }
1109
+
1110
+ function basePlaygroundUrl() {
1111
+ return location.href.split('#')[0];
1016
1112
  }
1017
1113
 
1018
- function restoreFromHash() {
1019
- if (!location.hash.startsWith('#state=')) return false;
1114
+ async function restoreFromHashOrLoadDefault() {
1115
+ if (!(await restoreFromHash())) await loadExample('ancestor');
1116
+ }
1117
+
1118
+ async function restoreFromHash() {
1119
+ if (!location.hash || location.hash === '#') return false;
1120
+ const params = new URLSearchParams(location.hash.slice(1));
1020
1121
  try {
1021
- const encoded = location.hash.slice(7).replaceAll('-', '+').replaceAll('_', '/');
1022
- const padded = encoded + '='.repeat((4 - encoded.length % 4) % 4);
1023
- const bytes = Uint8Array.from(atob(padded), (ch) => ch.charCodeAt(0));
1024
- const payload = JSON.parse(new TextDecoder().decode(bytes));
1025
- setSource(String(payload.source || ''), 'shared program');
1026
- proof.checked = Boolean(payload.proof);
1027
- stats.checked = Boolean(payload.stats);
1028
- backgroundSource = String(payload.backgroundSource || '');
1029
- backgroundName = String(payload.backgroundName || 'shared background');
1030
- updateBackgroundStatus();
1031
- setStatus('Loaded shared program.');
1032
- return true;
1122
+ if (params.has('state')) {
1123
+ applySharedState(decodeState(params.get('state')), 'shared program');
1124
+ setStatus('Loaded shared program.');
1125
+ return true;
1126
+ }
1127
+ if (params.has('state-url')) {
1128
+ const stateUrl = params.get('state-url');
1129
+ const response = await fetch(stateUrl);
1130
+ if (!response.ok) throw new Error(`${response.status} ${response.statusText}`);
1131
+ applySharedState(await response.json(), `shared state from ${stateUrl}`);
1132
+ setStatus('Loaded shared Gist state.');
1133
+ return true;
1134
+ }
1135
+ await restoreBackgroundUrl(params);
1136
+ applyOptionParams(params);
1137
+ if (params.has('example')) {
1138
+ await loadExample(params.get('example'));
1139
+ applyOptionParams(params);
1140
+ return true;
1141
+ }
1142
+ if (params.has('url')) {
1143
+ await loadSourceFromUrl(params.get('url'), false);
1144
+ applyOptionParams(params);
1145
+ return true;
1146
+ }
1033
1147
  } catch (error) {
1034
1148
  setStatus(`Could not read shared state: ${formatError(error)}`, true);
1035
1149
  return false;
1036
1150
  }
1151
+ return false;
1152
+ }
1153
+
1154
+ async function restoreBackgroundUrl(params) {
1155
+ const url = params.get('background-url');
1156
+ if (!url) return;
1157
+ await loadSourceFromUrl(url, true);
1158
+ }
1159
+
1160
+ async function loadSourceFromUrl(url, asBackground) {
1161
+ const response = await fetch(url);
1162
+ if (!response.ok) throw new Error(`${response.status} ${response.statusText}`);
1163
+ const text = await response.text();
1164
+ if (asBackground) {
1165
+ backgroundSource = text;
1166
+ backgroundName = url;
1167
+ backgroundReference = { kind: 'url', url };
1168
+ updateBackgroundStatus();
1169
+ } else {
1170
+ setSource(text, url, { kind: 'url', url });
1171
+ }
1172
+ }
1173
+
1174
+ function applySharedState(payload, label) {
1175
+ setSource(String(payload.source || ''), label, { kind: 'custom' });
1176
+ proof.checked = Boolean(payload.proof);
1177
+ stats.checked = Boolean(payload.stats);
1178
+ backgroundSource = String(payload.backgroundSource || '');
1179
+ backgroundName = String(payload.backgroundName || 'shared background');
1180
+ backgroundReference = null;
1181
+ updateBackgroundStatus();
1182
+ }
1183
+
1184
+ function applyOptionParams(params) {
1185
+ proof.checked = params.get('proof') === '1' || params.get('proof') === 'true';
1186
+ stats.checked = params.get('stats') === '1' || params.get('stats') === 'true';
1037
1187
  }
1038
1188
 
1039
1189
  async function loadVersion() {
@@ -678,6 +678,15 @@ function playgroundStaticIssues() {
678
678
  if (!html.includes('id="line-numbers"') || !html.includes('updateLineNumbers') || !html.includes('lineNumbersInner.style.transform') || !html.includes('--line-number-bg')) {
679
679
  issues.push('playground editor must include synced line numbers');
680
680
  }
681
+ if (!html.includes('MAX_SHARE_URL_LENGTH') || !html.includes('buildReferenceShareLink') || !html.includes("params.set('example'") || !html.includes("params.set('url'")) {
682
+ issues.push('playground share links must avoid embedding large example or URL-loaded sources');
683
+ }
684
+ if (!html.includes('id="create-gist"') || !html.includes('createGistShare') || !html.includes('GIST_STATE_FILENAME') || !html.includes("fetch('https://api.github.com/gists'")) {
685
+ issues.push('playground must support Gist-backed sharing for large programs');
686
+ }
687
+ if (!html.includes("params.has('state-url')") || !html.includes('#state-url=')) {
688
+ issues.push('playground must restore state from raw Gist state URLs');
689
+ }
681
690
  if (!html.includes('id="example-search"') || !html.includes('id="examples"')) issues.push('playground must include searchable examples');
682
691
  const scriptMatch = html.match(new RegExp('<script type="module">\\n([\\s\\S]*?)\\n <\\/script>'));
683
692
  if (scriptMatch == null) {