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 +1 -1
- package/playground.html +173 -23
- package/test/run-regression.mjs +9 -0
package/package.json
CHANGED
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
1015
|
-
|
|
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
|
|
1019
|
-
if (!
|
|
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
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
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() {
|
package/test/run-regression.mjs
CHANGED
|
@@ -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) {
|