eyeling 1.24.20 → 1.24.24

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/HANDBOOK.md CHANGED
@@ -3733,7 +3733,7 @@ The output behavior also adapts to the kind of N3 program being run. In some cas
3733
3733
 
3734
3734
  For Markdown-oriented `log:outputString` examples, the output pane has two views: a rendered Markdown view and a Markdown source view. Those tabs appear only when the actual output looks like Markdown; Turtle or other plain output stays in the source editor without the Markdown toggle. The rendered view is selected by default for Markdown output, while the source view keeps the exact generated Markdown available for copying, inspection, or comparison.
3735
3735
 
3736
- Repository example reports often contain relative source links that are written for the checked-in files under `examples/output/*.md`. When such an example is loaded into the playground from a raw URL, the rendered Markdown view resolves those relative links against the corresponding static output page rather than against `/playground`, so links like `../name.n3` and `../input/name.trig` continue to point to the intended example files.
3736
+ Repository example reports often contain relative source links that are written for the checked-in files under `examples/output/*.md`. When such an example is loaded into the playground from a raw URL, or restored later from compact/Gist-backed state, the rendered Markdown view resolves those relative links against the corresponding static output page rather than against `/playground`, so links like `../name.n3` and `../input/name.trig` continue to point to the intended example files. The playground preserves this base in shared state when possible and can also recover it from the injected `@base <.../examples/name.n3>` line.
3737
3737
 
3738
3738
  ### I.4 Error handling and explainability
3739
3739
 
@@ -3760,11 +3760,11 @@ When a user does want a portable link, the **Copy share link** button creates on
3760
3760
  - edited programs are shared with a compact compressed `?state=...` payload,
3761
3761
  - default option values are omitted from that payload to keep links small.
3762
3762
 
3763
- If a generated compact share link is still very long, the playground reveals a **Create TinyURL** option. The threshold is intentionally conservative, and the shortener handoff is explicit rather than automatic: after the user chooses that option, the browser uses a TinyURL API token stored locally in that browser to create the short link and copy it to the clipboard. The API request is sent as a POST with `referrerPolicy: "no-referrer"`, so an already-long playground URL is not leaked again as a large `Referer` header. If no token is provided, or if the API request fails, the playground falls back to opening TinyURL without putting the long URL in TinyURL's address bar, and it copies the compact share link so it can be pasted manually. This avoids silently sending encoded editor content to a third-party service while still making the account-backed TinyURL workflow one click after setup.
3763
+ If a generated embedded-state link is still very long, the playground reveals **Create Gist share**. That option asks for a GitHub token with gist permission, stores the compact playground state as a secret Gist JSON file, and copies a small `?stateurl=...` playground link that fetches the state file client-side. The token is stored only in that browser's `localStorage`, the Gist is created by a POST request with `referrerPolicy: "no-referrer"`, and the shared playground URL no longer contains the large encoded program.
3764
3764
 
3765
3765
  This keeps everyday use pleasant while preserving the important tutorial and issue-reporting workflow: a link can still capture the imported resource, the local editable overlay, background-knowledge mode, proof-comments mode, and HTTPS-dereferencing mode.
3766
3766
 
3767
- For compatibility, older `?edit=`, `?program=`, `?url=`, compact `?state=`, and hash-based links are still accepted when opened. The old `/demo` entry point is also kept as a redirect to the canonical `/playground` page.
3767
+ For compatibility, older `?edit=`, `?program=`, `?url=`, compact `?state=`, `?stateurl=`, and hash-based links are accepted when opened. The old `/demo` entry point is also kept as a redirect to the canonical `/playground` page.
3768
3768
 
3769
3769
  ### I.6 What the playground is good for
3770
3770
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eyeling",
3
- "version": "1.24.20",
3
+ "version": "1.24.24",
4
4
  "description": "A minimal Notation3 (N3) reasoner in JavaScript.",
5
5
  "main": "./index.js",
6
6
  "keywords": [
@@ -722,7 +722,8 @@ async function main() {
722
722
  renderedTabSelected: renderedTab ? renderedTab.getAttribute('aria-selected') === 'true' : false,
723
723
  sourceTabSelected: sourceTab ? sourceTab.getAttribute('aria-selected') === 'true' : false,
724
724
  shareStatus: document.getElementById('share-status') ? String(document.getElementById('share-status').textContent || '') : '',
725
- shortenerHidden: document.getElementById('open-shortener-btn') ? !!document.getElementById('open-shortener-btn').hidden : true,
725
+ gistShareHidden: document.getElementById('create-gist-share-btn') ? !!document.getElementById('create-gist-share-btn').hidden : true,
726
+ gistShareText: document.getElementById('create-gist-share-btn') ? String(document.getElementById('create-gist-share-btn').textContent || '').trim() : '',
726
727
  backgroundStatus: document.getElementById('background-status') ? String(document.getElementById('background-status').textContent || '') : '',
727
728
  href: String(window.location.href || ''),
728
729
  highlighted,
@@ -783,15 +784,16 @@ async function main() {
783
784
  return {
784
785
  url,
785
786
  length: url.length,
786
- threshold: window.__eyelingPlaygroundShareUrlShortenerThreshold,
787
- needsShortener: window.__eyelingPlaygroundShouldOfferShortener(url),
788
- shortenerUrl: window.__eyelingPlaygroundMakeShareUrlShortenerUrl(url),
787
+ threshold: window.__eyelingPlaygroundGistShareThreshold,
788
+ needsGistShare: window.__eyelingPlaygroundShouldOfferGistShare(url),
789
+ hasEmbeddedState: window.__eyelingPlaygroundShareUrlHasEmbeddedState(url),
790
+ stateUrlShare: window.__eyelingPlaygroundMakeShareUrlFromStateUrl('https://gist.githubusercontent.com/user/id/raw/eyeling-playground-state.json'),
789
791
  };
790
792
  })()`);
791
793
  }
792
794
 
793
- async function createTinyUrlWithStubInPage(longUrl, token, response) {
794
- const payload = JSON.stringify({ longUrl: String(longUrl), token: String(token), response });
795
+ async function createGistBackedShareUrlWithStubInPage(token, response) {
796
+ const payload = JSON.stringify({ token: String(token), response });
795
797
  return await evalInPage(`(async () => {
796
798
  const args = ${payload};
797
799
  const originalFetch = window.fetch;
@@ -816,8 +818,15 @@ async function main() {
816
818
  };
817
819
  };
818
820
  try {
819
- const tinyUrl = await window.__eyelingPlaygroundCreateTinyUrl(args.longUrl, args.token);
820
- return { tinyUrl, seen };
821
+ const state = {
822
+ edit: (window.__cmStubsById && window.__cmStubsById['n3-editor']) ? window.__cmStubsById['n3-editor'].getValue() : '',
823
+ url: '',
824
+ loadbg: false,
825
+ proofcomments: false,
826
+ httpsderef: true,
827
+ };
828
+ const shareUrl = await window.__eyelingPlaygroundCreateGistBackedShareUrl(state, args.token);
829
+ return { shareUrl, seen };
821
830
  } finally {
822
831
  window.fetch = originalFetch;
823
832
  }
@@ -864,6 +873,11 @@ ${JSON.stringify(last, null, 2)}`);
864
873
  const outputStringProgram = `@prefix : <#> .
865
874
  @prefix log: <http://www.w3.org/2000/10/swap/log#> .
866
875
  :report log:outputString "## Hello from output string\n\nLine 2 with **bold** and [Eyeling](https://example.org/eyeling)\n" .
876
+ `;
877
+ const baseOnlyMarkdownProgram = `@base <https://raw.githubusercontent.com/eyereasoner/eyeling/refs/heads/main/examples/smoke-arithmetic.n3> .
878
+ @prefix : <#> .
879
+ @prefix log: <http://www.w3.org/2000/10/swap/log#> .
880
+ :report log:outputString "# stateurl link base\n\n[N3 rules](../smoke-arithmetic.n3)\n[Input TriG](../input/smoke-arithmetic.trig)\n" .
867
881
  `;
868
882
  const logQueryTurtleProgram = `@prefix : <#> .
869
883
  @prefix log: <http://www.w3.org/2000/10/swap/log#> .
@@ -963,18 +977,46 @@ ${JSON.stringify(last, null, 2)}`);
963
977
  assert.equal(renderedAgain.renderedTabSelected, true, 'Expected Rendered tab to be selectable again');
964
978
  endTest();
965
979
 
966
- // 5) Normal editing should not keep rewriting the browser URL with raw N3 content.
980
+ // 5) Shared state files may only restore editor text. If that text came from a repository
981
+ // example, the injected @base line should still give Markdown links the static output-page base.
982
+ beginTest('playground resolves Markdown links from restored example base directives');
983
+ await setProgram(baseOnlyMarkdownProgram);
984
+ await clickRun();
985
+ const baseOnlyMarkdown = await waitForState(
986
+ 'base-only Markdown output completion',
987
+ (st) =>
988
+ String(st.status || '')
989
+ .trim()
990
+ .startsWith('Done') && /stateurl link base/i.test(String(st.output || '')),
991
+ 20000,
992
+ );
993
+ assert.match(
994
+ baseOnlyMarkdown.renderedHtml,
995
+ new RegExp('href="' + started.baseUrl.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '/examples/smoke-arithmetic\\.n3"'),
996
+ 'Expected restored-state Markdown source links to resolve against the static output page',
997
+ );
998
+ assert.match(
999
+ baseOnlyMarkdown.renderedHtml,
1000
+ new RegExp('href="' + started.baseUrl.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '/examples/input/smoke-arithmetic\\.trig"'),
1001
+ 'Expected restored-state Markdown TriG links to resolve against the static output page',
1002
+ );
1003
+ endTest();
1004
+
1005
+ // 6) Normal editing should not keep rewriting the browser URL with raw N3 content.
967
1006
  beginTest('playground keeps the live URL short and creates compact share links on demand');
968
- assert.doesNotMatch(renderedAgain.href, /[?&](?:edit|program)=/, 'Expected live URL to avoid raw editor content');
1007
+ await setProgram(outputStringProgram);
1008
+ const compactShareState = await getPlaygroundState();
1009
+ assert.doesNotMatch(compactShareState.href, /[?&](?:edit|program)=/, 'Expected live URL to avoid raw editor content');
969
1010
  const compactShareUrl = await makeShareUrlInPage();
1011
+ const rawEditorUrlLength = playgroundUrl.length + '?edit='.length + encodeURIComponent(outputStringProgram).length;
970
1012
  assert.match(compactShareUrl, /[?&]state=/, 'Expected an on-demand compact state parameter');
971
1013
  assert.doesNotMatch(compactShareUrl, /[?&](?:edit|program)=/, 'Expected share link to avoid raw edit/program params');
972
- assert.ok(compactShareUrl.length < playgroundUrl.length + encodeURIComponent(outputStringProgram).length, 'Expected compact share URL to be shorter than raw editor URL');
973
- assert.equal(renderedAgain.shortenerHidden, true, 'Expected ordinary compact share links to keep the shortener option hidden');
1014
+ assert.ok(compactShareUrl.length < rawEditorUrlLength, 'Expected compact share URL to be shorter than raw editor URL');
1015
+ assert.equal(compactShareState.gistShareHidden, true, 'Expected ordinary compact share links to keep the Gist share option hidden');
974
1016
  endTest();
975
1017
 
976
- // 6) Very large edited programs should offer a URL shortener handoff instead of only a huge link.
977
- beginTest('playground offers a URL shortener option for oversized share links');
1018
+ // 7) Very large edited programs should offer a Gist-backed share option instead of only a huge link.
1019
+ beginTest('playground offers a Gist-backed option for oversized state links');
978
1020
  const longShareProgram = Array.from({ length: 1400 }, (_, i) => {
979
1021
  const n = String(i).padStart(4, '0');
980
1022
  const token = ((i * 2654435761) >>> 0).toString(36).padStart(7, '0');
@@ -983,21 +1025,30 @@ ${JSON.stringify(last, null, 2)}`);
983
1025
  await setProgram(longShareProgram);
984
1026
  const longShare = await makeShareUrlDiagnosticsInPage();
985
1027
  assert.ok(longShare.length > longShare.threshold, `Expected test share URL to exceed threshold (${longShare.length} <= ${longShare.threshold})`);
986
- assert.equal(longShare.needsShortener, true, 'Expected oversized share URL to request a shortener option');
987
- assert.equal(longShare.shortenerUrl, 'https://tinyurl.com/app', 'Expected TinyURL app fallback handoff to avoid embedding the huge URL');
988
- assert.equal(longShare.shortenerUrl.includes(encodeURIComponent(longShare.url).slice(0, 40)), false, 'Expected shortener fallback URL not to carry the generated share URL');
989
- const tinyUrlCreated = await createTinyUrlWithStubInPage(longShare.url, 'test-token-123', {
990
- data: { tiny_url: 'https://tinyurl.com/eyeling-test' },
1028
+ assert.equal(longShare.needsGistShare, true, 'Expected oversized embedded state to request a Gist-backed sharing option');
1029
+ assert.equal(longShare.hasEmbeddedState, true, 'Expected oversized edited program to be an embedded state link');
1030
+ assert.match(longShare.stateUrlShare, /[?&]stateurl=/, 'Expected stateurl= links to be supported for externally stored state');
1031
+ assert.doesNotMatch(longShare.stateUrlShare, /[?&]state=/, 'Expected externally stored state links to avoid embedded state payloads');
1032
+ const gistShare = await createGistBackedShareUrlWithStubInPage('github-gist-token-123', {
1033
+ files: {
1034
+ 'eyeling-playground-state.json': {
1035
+ raw_url: 'https://gist.githubusercontent.com/user/id/raw/eyeling-playground-state.json',
1036
+ },
1037
+ },
991
1038
  });
992
- assert.equal(tinyUrlCreated.tinyUrl, 'https://tinyurl.com/eyeling-test', 'Expected TinyURL API response to produce a short URL');
993
- assert.equal(tinyUrlCreated.seen.url, 'https://api.tinyurl.com/create', 'Expected TinyURL API create endpoint');
994
- assert.equal(tinyUrlCreated.seen.options.method, 'POST', 'Expected TinyURL API POST request');
995
- assert.equal(tinyUrlCreated.seen.options.headers.Authorization, 'Bearer test-token-123', 'Expected bearer token authorization');
996
- assert.equal(tinyUrlCreated.seen.options.referrerPolicy, 'no-referrer', 'Expected TinyURL API request not to send a long Referer');
997
- assert.match(String(tinyUrlCreated.seen.options.body || ''), /"url":/, 'Expected TinyURL API body to include the long URL');
1039
+ assert.match(gistShare.shareUrl, /[?&]stateurl=/, 'Expected Gist-backed share URL to use a compact stateurl parameter');
1040
+ assert.doesNotMatch(gistShare.shareUrl, /[?&]state=/, 'Expected Gist-backed share URL not to embed the compressed state');
1041
+ assert.ok(gistShare.shareUrl.length < 300, 'Expected Gist-backed share URL to stay small');
1042
+ assert.equal(gistShare.seen.url, 'https://api.github.com/gists', 'Expected GitHub Gist create endpoint');
1043
+ assert.equal(gistShare.seen.options.method, 'POST', 'Expected GitHub Gist API POST request');
1044
+ assert.equal(gistShare.seen.options.headers.Authorization, 'Bearer github-gist-token-123', 'Expected bearer token authorization');
1045
+ assert.equal(gistShare.seen.options.referrerPolicy, 'no-referrer', 'Expected GitHub Gist API request not to send a long Referer');
1046
+ assert.match(String(gistShare.seen.options.body || ''), /"public":false/, 'Expected a secret Gist, not a public Gist');
1047
+ assert.match(String(gistShare.seen.options.body || ''), /eyeling-playground-state\.json/, 'Expected shared state to be saved as JSON');
1048
+ assert.match(String(gistShare.seen.options.body || ''), /\\"e\\":/, 'Expected compact editor state in the Gist payload');
998
1049
  endTest();
999
1050
 
1000
- // 7) log:query can produce Turtle; that should stay in plain source output without Markdown tabs.
1051
+ // 8) log:query can produce Turtle; that should stay in plain source output without Markdown tabs.
1001
1052
  beginTest('playground hides markdown tabs for Turtle log:query output');
1002
1053
  await setProgram(logQueryTurtleProgram);
1003
1054
  await clickRun();
@@ -1017,7 +1068,7 @@ ${JSON.stringify(last, null, 2)}`);
1017
1068
  assert.equal(logQueryTurtle.sourceHidden, false, 'Expected Turtle log:query output to show source directly');
1018
1069
  endTest();
1019
1070
 
1020
- // 8) URL-loaded examples should auto-load matching examples/input/<stem>.trig and run in RDF/TriG mode.
1071
+ // 9) URL-loaded examples should auto-load matching examples/input/<stem>.trig and run in RDF/TriG mode.
1021
1072
  beginTest('playground auto-loads companion TriG sidecars and uses RDF/TriG mode');
1022
1073
  await loadUrlIntoEditor('https://raw.githubusercontent.com/eyereasoner/eyeling/refs/heads/main/examples/smoke-arithmetic.n3');
1023
1074
  const smokeLoaded = await waitForState(
@@ -1064,7 +1115,7 @@ ${JSON.stringify(last, null, 2)}`);
1064
1115
  assert.equal(smokeRenderedAgain.sourceHidden, true, 'Expected smoke-arithmetic source editor to hide again');
1065
1116
  endTest();
1066
1117
 
1067
- // 9) URL-loaded repository examples should auto-load matching examples/builtin/<stem>.js.
1118
+ // 10) URL-loaded repository examples should auto-load matching examples/builtin/<stem>.js.
1068
1119
  beginTest('playground auto-loads a companion example builtin for URL-loaded Sudoku');
1069
1120
  await loadUrlIntoEditor('https://raw.githubusercontent.com/eyereasoner/eyeling/refs/heads/main/examples/sudoku.n3');
1070
1121
  await waitForState(