From 4e2131f2f742121e41158373438700f76ab8a7c5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 12 Sep 2026 16:02:28 +0000 Subject: [PATCH 1/5] Initial plan From b99711ec079ac3c56f2cd6d7bd4a5a4dac8112b7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 12 Sep 2026 16:08:09 +0000 Subject: [PATCH 2/5] feat: CTRL+SHIFT+CLICK on URI looks up triples where URI is object --- packages/yasgui/src/Tab.ts | 12 +++++- packages/yasqe/src/index.ts | 5 ++- packages/yasr/src/index.ts | 1 + packages/yasr/src/plugins/response/index.ts | 39 ++++++++++++++++++- .../yasr/src/plugins/response/uriUtils.ts | 12 +++++- test/unit/response-uri-utils-test.ts | 20 ++++++++-- 6 files changed, 80 insertions(+), 9 deletions(-) diff --git a/packages/yasgui/src/Tab.ts b/packages/yasgui/src/Tab.ts index 7de2d106..30a323e0 100644 --- a/packages/yasgui/src/Tab.ts +++ b/packages/yasgui/src/Tab.ts @@ -9,6 +9,7 @@ import { Config as YasrConfig, PersistentConfig as YasrPersistentConfig, PluginQueryOptions as YasrPluginQueryOptions, + buildObjectOfQuery, } from "@matdata/yasr"; import { mapValues, eq, mergeWith, words, deburr, invert } from "lodash-es"; import * as shareLink from "./linkUtils"; @@ -1564,8 +1565,14 @@ export class Tab extends EventEmitter { } } - // Construct the query - const constructQuery = `CONSTRUCT { + // Construct the query based on whether Shift is held + let constructQuery: string; + if (event.shiftKey) { + // Ctrl+Shift+Click: find all triples where URI is the object + constructQuery = buildObjectOfQuery(uri); + } else { + // Ctrl+Click: find all triples where URI is subject or object + constructQuery = `CONSTRUCT { ?s_left ?p_left ?target . ?target ?p_right ?o_right . } @@ -1579,6 +1586,7 @@ WHERE { ?target ?p_right ?o_right . } } LIMIT 1000`; + } // Execute query in background without changing editor content // Note: void operator is intentional - errors are handled in the catch block of executeBackgroundQuery diff --git a/packages/yasqe/src/index.ts b/packages/yasqe/src/index.ts index 84479da7..a34b0cb0 100644 --- a/packages/yasqe/src/index.ts +++ b/packages/yasqe/src/index.ts @@ -228,7 +228,10 @@ export class Yasqe extends EditorFacade { if (token && token.type === "variable-3") { // Token type "variable-3" represents URIs (IRI_REF) - this.showNotification("uri-describe-hint", "Hold CTRL - left mouse click on URI to DESCRIBE"); + this.showNotification( + "uri-describe-hint", + "CTRL+click: find triples where URI is subject or object. CTRL+SHIFT+click: find triples where URI is object.", + ); } else { this.hideNotification("uri-describe-hint"); } diff --git a/packages/yasr/src/index.ts b/packages/yasr/src/index.ts index 2e4292f9..ea2d3c40 100644 --- a/packages/yasr/src/index.ts +++ b/packages/yasr/src/index.ts @@ -731,5 +731,6 @@ Yasr.registerPlugin("response", YasrPluginResponse.default as any); Yasr.registerPlugin("error", YasrPluginError.default as any); export type { Plugin, DownloadInfo } from "./plugins"; +export { buildObjectOfQuery } from "./plugins/response/uriUtils"; export default Yasr; diff --git a/packages/yasr/src/plugins/response/index.ts b/packages/yasr/src/plugins/response/index.ts index 42cf546c..f93942f6 100644 --- a/packages/yasr/src/plugins/response/index.ts +++ b/packages/yasr/src/plugins/response/index.ts @@ -14,7 +14,7 @@ import { turtle } from "codemirror-lang-turtle"; import { javascript } from "@codemirror/legacy-modes/mode/javascript"; import { addClass, removeClass } from "@matdata/yasgui-utils"; import { DeepReadonly } from "ts-essentials"; -import { extractUriAtOffset, buildDescribeQuery } from "./uriUtils"; +import { extractUriAtOffset, buildDescribeQuery, buildObjectOfQuery } from "./uriUtils"; export interface PluginConfig { maxLines: number; @@ -132,6 +132,8 @@ export default class Response implements Plugin { /** * Ctrl/Cmd+Click on a URI in the response view runs a `DESCRIBE` query for that * URI and appends the result to the response view (similar to the graph plugin). + * Ctrl/Cmd+Shift+Click runs a CONSTRUCT query to find all triples where the URI + * is the object. */ private handleMouseDown = (event: MouseEvent) => { if (!event.ctrlKey && !event.metaKey) return; @@ -148,7 +150,12 @@ export default class Response implements Plugin { event.preventDefault(); event.stopPropagation(); - void this.describeUri(uri); + + if (event.shiftKey) { + void this.runObjectOfQuery(uri); + } else { + void this.describeUri(uri); + } }; private async describeUri(uri: string) { @@ -179,6 +186,34 @@ export default class Response implements Plugin { } } + private async runObjectOfQuery(uri: string) { + if (!this.yasr.config.executeQuery) return; + const cmAtStart = this.cm; + if (!cmAtStart) return; + + const query = buildObjectOfQuery(uri); + try { + this.yasr.showLoading(); + const response = await this.yasr.executeQuery(query, { acceptHeader: this.getDescribeAcceptHeader() }); + if (this.cm !== cmAtStart) return; + + const content = this.getResponseContent(response); + if (content) { + this.appendToView(`\n\n# Triples where <${uri}> is object\n${content.trim()}\n`); + } else { + this.appendToView(`\n\n# Triples where <${uri}> is object: no data\n`); + } + } catch (error) { + if (this.cm !== cmAtStart) return; + + console.error("Object-of query failed:", error); + const message = error instanceof Error ? error.message : String(error); + this.appendToView(`\n\n# Triples where <${uri}> is object failed: ${message}\n`); + } finally { + if (this.cm === cmAtStart) this.yasr.hideLoading(); + } + } + /** * Pick an Accept header for the DESCRIBE request. When the current response is an * RDF graph format, reuse it so the appended triples match what is already shown. diff --git a/packages/yasr/src/plugins/response/uriUtils.ts b/packages/yasr/src/plugins/response/uriUtils.ts index 7fee89f2..b7c22758 100644 --- a/packages/yasr/src/plugins/response/uriUtils.ts +++ b/packages/yasr/src/plugins/response/uriUtils.ts @@ -60,8 +60,18 @@ export function extractUriAtOffset(text: string, offset: number): string | undef * Build a `DESCRIBE` query for the given URI. * * @param uri The URI to describe. - * @returns A SPARQL `DESCRIBE` query string. + * @returns A SPARQL `CONSTRUCT` query string that retrieves all triples where the URI is the subject. */ export function buildDescribeQuery(uri: string): string { return `CONSTRUCT { <${uri}> ?p ?o } WHERE { <${uri}> ?p ?o }`; } + +/** + * Build a CONSTRUCT query that retrieves all triples where the given URI is the object. + * + * @param uri The URI to look up as an object. + * @returns A SPARQL `CONSTRUCT` query string. + */ +export function buildObjectOfQuery(uri: string): string { + return `CONSTRUCT { ?s ?p <${uri}> } WHERE { ?s ?p <${uri}> }`; +} diff --git a/test/unit/response-uri-utils-test.ts b/test/unit/response-uri-utils-test.ts index ef47c13e..a7870f7c 100644 --- a/test/unit/response-uri-utils-test.ts +++ b/test/unit/response-uri-utils-test.ts @@ -1,7 +1,11 @@ import * as chai from "chai"; import { describe, it } from "mocha"; -import { extractUriAtOffset, buildDescribeQuery } from "../../packages/yasr/src/plugins/response/uriUtils.js"; +import { + extractUriAtOffset, + buildDescribeQuery, + buildObjectOfQuery, +} from "../../packages/yasr/src/plugins/response/uriUtils.js"; const expect = chai.expect; @@ -53,8 +57,18 @@ describe("Response plugin URI utilities", () => { }); describe("buildDescribeQuery", () => { - it("wraps the URI in a DESCRIBE query", () => { - expect(buildDescribeQuery("http://example.org/foo")).to.equal("DESCRIBE "); + it("wraps the URI in a CONSTRUCT query retrieving triples where the URI is subject", () => { + expect(buildDescribeQuery("http://example.org/foo")).to.equal( + "CONSTRUCT { ?p ?o } WHERE { ?p ?o }", + ); + }); + }); + + describe("buildObjectOfQuery", () => { + it("wraps the URI in a CONSTRUCT query retrieving triples where the URI is object", () => { + expect(buildObjectOfQuery("http://example.org/foo")).to.equal( + "CONSTRUCT { ?s ?p } WHERE { ?s ?p }", + ); }); }); }); From a8a5a20c681794e5ffb2f26778e65b7547514999 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 12 Sep 2026 16:09:43 +0000 Subject: [PATCH 3/5] fix: add LIMIT 1000 to buildObjectOfQuery for consistency with Ctrl+Click query --- packages/yasr/src/plugins/response/uriUtils.ts | 2 +- test/unit/response-uri-utils-test.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/yasr/src/plugins/response/uriUtils.ts b/packages/yasr/src/plugins/response/uriUtils.ts index b7c22758..556ed34b 100644 --- a/packages/yasr/src/plugins/response/uriUtils.ts +++ b/packages/yasr/src/plugins/response/uriUtils.ts @@ -73,5 +73,5 @@ export function buildDescribeQuery(uri: string): string { * @returns A SPARQL `CONSTRUCT` query string. */ export function buildObjectOfQuery(uri: string): string { - return `CONSTRUCT { ?s ?p <${uri}> } WHERE { ?s ?p <${uri}> }`; + return `CONSTRUCT { ?s ?p <${uri}> } WHERE { ?s ?p <${uri}> } LIMIT 1000`; } diff --git a/test/unit/response-uri-utils-test.ts b/test/unit/response-uri-utils-test.ts index a7870f7c..3ab4f9be 100644 --- a/test/unit/response-uri-utils-test.ts +++ b/test/unit/response-uri-utils-test.ts @@ -67,7 +67,7 @@ describe("Response plugin URI utilities", () => { describe("buildObjectOfQuery", () => { it("wraps the URI in a CONSTRUCT query retrieving triples where the URI is object", () => { expect(buildObjectOfQuery("http://example.org/foo")).to.equal( - "CONSTRUCT { ?s ?p } WHERE { ?s ?p }", + "CONSTRUCT { ?s ?p } WHERE { ?s ?p } LIMIT 1000", ); }); }); From c9a55d4b0d040cc7e8be0072e5b20038f0d1b3d1 Mon Sep 17 00:00:00 2001 From: Mathias Vanden Auweele Date: Sat, 12 Sep 2026 18:44:56 +0200 Subject: [PATCH 4/5] fix: harmonise uri exploration features --- docs/user-guide.md | 36 ++++++++++--------- packages/yasgui/src/Tab.ts | 18 ++-------- packages/yasgui/src/TabSettingsModal.ts | 3 +- packages/yasqe/src/index.ts | 2 +- packages/yasr/src/index.ts | 2 +- packages/yasr/src/plugins/response/index.ts | 28 +++++++-------- .../yasr/src/plugins/response/uriUtils.ts | 10 +++--- test/unit/response-uri-utils-test.ts | 6 ++-- 8 files changed, 47 insertions(+), 58 deletions(-) diff --git a/docs/user-guide.md b/docs/user-guide.md index 5e4634ee..e432b264 100644 --- a/docs/user-guide.md +++ b/docs/user-guide.md @@ -712,19 +712,19 @@ PREFIX rdfs: """ ; ### URI Explorer -Quickly explore RDF resources by Ctrl+clicking on URIs in your query. +Quickly explore RDF resources by Ctrl+(Shift+)clicking on URIs in your query. **How It Works:** 1. Hold `Ctrl` and click any URI in the query editor -2. Matgui automatically generates and executes a CONSTRUCT query exploring: - - Outgoing triples (where the URI is the subject) - - Incoming triples (where the URI is the object) +2. MatGUI automatically generates and executes a CONSTRUCT query where the URI is the **subject** 3. Results appear without modifying your original query 4. View the resource's connections in the results viewer +Alternatively, use `Ctrl`+`Shift` and click on the URI to executes a CONSTRUCT query where the URI is the **object**. + **Example:** -Ctrl+clicking on `http://dbpedia.org/resource/European_Union` automatically queries for all triples related to the European Union. +Ctrl+clicking on `http://dbpedia.org/resource/European_Union` automatically queries for all triples where the European Union is the subject of. ### Query Tabs @@ -1396,7 +1396,8 @@ Shows the raw response from the SPARQL endpoint. - Code folding for nested structures - Copy to clipboard functionality - Line numbers -- **Ctrl+Click on a URI** to run a `DESCRIBE` query for that resource and append the returned triples to the response view (similar to the Graph plugin's node expansion) +- **Ctrl+Click on a URI** executes a CONSTRUCT query where the URI is the **subject** and append the returned triples to the response view (similar to the Graph plugin's node expansion) +- **Ctrl+Click on a URI** executes a CONSTRUCT query where the URI is the **object** and append the returned triples to the response view (similar to the Graph plugin's node expansion) **Best For:** @@ -1800,17 +1801,18 @@ Master Matgui with these keyboard shortcuts for faster querying. ### Query Editor (YASQE) -| Shortcut | Action | -| -------------------------- | -------------------------------- | -| `Ctrl+Enter` / `Cmd+Enter` | Execute query | -| `Ctrl+Space` | Trigger autocomplete | -| `Ctrl+S` | Save query to local storage | -| `Ctrl+Shift+F` | Format query | -| `Ctrl+/` | Toggle comment on selected lines | -| `Ctrl+Shift+D` | Duplicate current line | -| `Ctrl+Shift+K` | Delete current line | -| `Esc` | Remove focus from editor | -| `Ctrl+Click` (on URI) | Explore URI connections | +| Shortcut | Action | +| --------------------------- | -------------------------------- | +| `Ctrl+Enter` / `Cmd+Enter` | Execute query | +| `Ctrl+Space` | Trigger autocomplete | +| `Ctrl+S` | Save query to local storage | +| `Ctrl+Shift+F` | Format query | +| `Ctrl+/` | Toggle comment on selected lines | +| `Ctrl+Shift+D` | Duplicate current line | +| `Ctrl+Shift+K` | Delete current line | +| `Esc` | Remove focus from editor | +| `Ctrl+Click` (on URI) | Explore outgoing URI connections | +| `Ctrl+Shift+Click` (on URI) | Explore incoming URI connections | ### Fullscreen diff --git a/packages/yasgui/src/Tab.ts b/packages/yasgui/src/Tab.ts index 30a323e0..7bb54002 100644 --- a/packages/yasgui/src/Tab.ts +++ b/packages/yasgui/src/Tab.ts @@ -9,6 +9,7 @@ import { Config as YasrConfig, PersistentConfig as YasrPersistentConfig, PluginQueryOptions as YasrPluginQueryOptions, + buildSubjectOfQuery, buildObjectOfQuery, } from "@matdata/yasr"; import { mapValues, eq, mergeWith, words, deburr, invert } from "lodash-es"; @@ -1571,21 +1572,8 @@ export class Tab extends EventEmitter { // Ctrl+Shift+Click: find all triples where URI is the object constructQuery = buildObjectOfQuery(uri); } else { - // Ctrl+Click: find all triples where URI is subject or object - constructQuery = `CONSTRUCT { - ?s_left ?p_left ?target . - ?target ?p_right ?o_right . -} -WHERE { - BIND(<${uri}> as ?target) - { - ?s_left ?p_left ?target . - } - UNION - { - ?target ?p_right ?o_right . - } -} LIMIT 1000`; + // Ctrl+Click: find all triples where URI is the subject + constructQuery = buildSubjectOfQuery(uri); } // Execute query in background without changing editor content diff --git a/packages/yasgui/src/TabSettingsModal.ts b/packages/yasgui/src/TabSettingsModal.ts index 19456af6..bd1fa79a 100644 --- a/packages/yasgui/src/TabSettingsModal.ts +++ b/packages/yasgui/src/TabSettingsModal.ts @@ -2032,7 +2032,8 @@ export default class TabSettingsModal { { keys: ["Ctrl+Shift+D", "Cmd+Shift+D"], description: "Duplicate current line" }, { keys: ["Ctrl+Shift+K", "Cmd+Shift+K"], description: "Delete current line" }, { keys: ["Esc"], description: "Remove focus from editor" }, - { keys: ["Ctrl+Click"], description: "Explore URI connections (on URI)" }, + { keys: ["Ctrl+Click"], description: "Find triples where URI is subject" }, + { keys: ["Ctrl+Shift+Click"], description: "Find triples where URI is object" }, ], }, { diff --git a/packages/yasqe/src/index.ts b/packages/yasqe/src/index.ts index a34b0cb0..8bf71602 100644 --- a/packages/yasqe/src/index.ts +++ b/packages/yasqe/src/index.ts @@ -230,7 +230,7 @@ export class Yasqe extends EditorFacade { // Token type "variable-3" represents URIs (IRI_REF) this.showNotification( "uri-describe-hint", - "CTRL+click: find triples where URI is subject or object. CTRL+SHIFT+click: find triples where URI is object.", + "CTRL+click: find triples where URI is subject. CTRL+SHIFT+click: find triples where URI is object.", ); } else { this.hideNotification("uri-describe-hint"); diff --git a/packages/yasr/src/index.ts b/packages/yasr/src/index.ts index ea2d3c40..b35fa460 100644 --- a/packages/yasr/src/index.ts +++ b/packages/yasr/src/index.ts @@ -731,6 +731,6 @@ Yasr.registerPlugin("response", YasrPluginResponse.default as any); Yasr.registerPlugin("error", YasrPluginError.default as any); export type { Plugin, DownloadInfo } from "./plugins"; -export { buildObjectOfQuery } from "./plugins/response/uriUtils"; +export { buildSubjectOfQuery, buildObjectOfQuery } from "./plugins/response/uriUtils"; export default Yasr; diff --git a/packages/yasr/src/plugins/response/index.ts b/packages/yasr/src/plugins/response/index.ts index f93942f6..80acb370 100644 --- a/packages/yasr/src/plugins/response/index.ts +++ b/packages/yasr/src/plugins/response/index.ts @@ -14,7 +14,7 @@ import { turtle } from "codemirror-lang-turtle"; import { javascript } from "@codemirror/legacy-modes/mode/javascript"; import { addClass, removeClass } from "@matdata/yasgui-utils"; import { DeepReadonly } from "ts-essentials"; -import { extractUriAtOffset, buildDescribeQuery, buildObjectOfQuery } from "./uriUtils"; +import { extractUriAtOffset, buildSubjectOfQuery, buildObjectOfQuery } from "./uriUtils"; export interface PluginConfig { maxLines: number; @@ -130,10 +130,8 @@ export default class Response implements Plugin { } /** - * Ctrl/Cmd+Click on a URI in the response view runs a `DESCRIBE` query for that - * URI and appends the result to the response view (similar to the graph plugin). - * Ctrl/Cmd+Shift+Click runs a CONSTRUCT query to find all triples where the URI - * is the object. + * Ctrl/Cmd+Click on a URI runs a CONSTRUCT query for triples where the URI is the subject. + * Ctrl/Cmd+Shift+Click runs a CONSTRUCT query for triples where the URI is the object. */ private handleMouseDown = (event: MouseEvent) => { if (!event.ctrlKey && !event.metaKey) return; @@ -154,33 +152,33 @@ export default class Response implements Plugin { if (event.shiftKey) { void this.runObjectOfQuery(uri); } else { - void this.describeUri(uri); + void this.runSubjectOfQuery(uri); } }; - private async describeUri(uri: string) { + private async runSubjectOfQuery(uri: string) { if (!this.yasr.config.executeQuery) return; const cmAtStart = this.cm; if (!cmAtStart) return; - const query = buildDescribeQuery(uri); + const query = buildSubjectOfQuery(uri); try { this.yasr.showLoading(); - const response = await this.yasr.executeQuery(query, { acceptHeader: this.getDescribeAcceptHeader() }); + const response = await this.yasr.executeQuery(query, { acceptHeader: this.getConstructAcceptHeader() }); if (this.cm !== cmAtStart) return; const content = this.getResponseContent(response); if (content) { - this.appendToView(`\n\n# DESCRIBE <${uri}>\n${content.trim()}\n`); + this.appendToView(`\n\n# Triples where <${uri}> is subject\n${content.trim()}\n`); } else { - this.appendToView(`\n\n# DESCRIBE <${uri}> returned no data\n`); + this.appendToView(`\n\n# Triples where <${uri}> is subject: no data\n`); } } catch (error) { if (this.cm !== cmAtStart) return; - console.error("DESCRIBE query failed:", error); + console.error("Subject-of query failed:", error); const message = error instanceof Error ? error.message : String(error); - this.appendToView(`\n\n# DESCRIBE <${uri}> failed: ${message}\n`); + this.appendToView(`\n\n# Triples where <${uri}> is subject failed: ${message}\n`); } finally { if (this.cm === cmAtStart) this.yasr.hideLoading(); } @@ -194,7 +192,7 @@ export default class Response implements Plugin { const query = buildObjectOfQuery(uri); try { this.yasr.showLoading(); - const response = await this.yasr.executeQuery(query, { acceptHeader: this.getDescribeAcceptHeader() }); + const response = await this.yasr.executeQuery(query, { acceptHeader: this.getConstructAcceptHeader() }); if (this.cm !== cmAtStart) return; const content = this.getResponseContent(response); @@ -218,7 +216,7 @@ export default class Response implements Plugin { * Pick an Accept header for the DESCRIBE request. When the current response is an * RDF graph format, reuse it so the appended triples match what is already shown. */ - private getDescribeAcceptHeader(): string { + private getConstructAcceptHeader(): string { const contentType = this.yasr.results?.getContentType(); if (contentType) { const lower = contentType.toLowerCase(); diff --git a/packages/yasr/src/plugins/response/uriUtils.ts b/packages/yasr/src/plugins/response/uriUtils.ts index 556ed34b..ea5e73d5 100644 --- a/packages/yasr/src/plugins/response/uriUtils.ts +++ b/packages/yasr/src/plugins/response/uriUtils.ts @@ -1,5 +1,5 @@ /** - * Standalone helpers for the Response plugin's "Ctrl+Click a URI to DESCRIBE" feature. + * Standalone helpers for the Response plugin's Ctrl+Click URI exploration feature. * * These functions are intentionally free of any DOM or CodeMirror dependencies so * that they can be unit-tested in isolation. @@ -57,12 +57,12 @@ export function extractUriAtOffset(text: string, offset: number): string | undef } /** - * Build a `DESCRIBE` query for the given URI. + * Build a CONSTRUCT query that retrieves all triples where the given URI is the subject. * - * @param uri The URI to describe. - * @returns A SPARQL `CONSTRUCT` query string that retrieves all triples where the URI is the subject. + * @param uri The URI to look up as a subject. + * @returns A SPARQL `CONSTRUCT` query string. */ -export function buildDescribeQuery(uri: string): string { +export function buildSubjectOfQuery(uri: string): string { return `CONSTRUCT { <${uri}> ?p ?o } WHERE { <${uri}> ?p ?o }`; } diff --git a/test/unit/response-uri-utils-test.ts b/test/unit/response-uri-utils-test.ts index 3ab4f9be..a23d6226 100644 --- a/test/unit/response-uri-utils-test.ts +++ b/test/unit/response-uri-utils-test.ts @@ -3,7 +3,7 @@ import { describe, it } from "mocha"; import { extractUriAtOffset, - buildDescribeQuery, + buildSubjectOfQuery, buildObjectOfQuery, } from "../../packages/yasr/src/plugins/response/uriUtils.js"; @@ -56,9 +56,9 @@ describe("Response plugin URI utilities", () => { }); }); - describe("buildDescribeQuery", () => { + describe("buildSubjectOfQuery", () => { it("wraps the URI in a CONSTRUCT query retrieving triples where the URI is subject", () => { - expect(buildDescribeQuery("http://example.org/foo")).to.equal( + expect(buildSubjectOfQuery("http://example.org/foo")).to.equal( "CONSTRUCT { ?p ?o } WHERE { ?p ?o }", ); }); From bf580d7eba571aea74ee65157e06e04246931a39 Mon Sep 17 00:00:00 2001 From: Mathias Vanden Auweele Date: Sat, 12 Sep 2026 18:54:51 +0200 Subject: [PATCH 5/5] feat: strip duplicate prefix declarations from uri explorer feature --- packages/yasr/src/plugins/response/index.ts | 20 ++++++---- .../yasr/src/plugins/response/uriUtils.ts | 40 +++++++++++++++++++ test/unit/response-uri-utils-test.ts | 25 ++++++++++++ 3 files changed, 78 insertions(+), 7 deletions(-) diff --git a/packages/yasr/src/plugins/response/index.ts b/packages/yasr/src/plugins/response/index.ts index 80acb370..b60b24b4 100644 --- a/packages/yasr/src/plugins/response/index.ts +++ b/packages/yasr/src/plugins/response/index.ts @@ -14,7 +14,12 @@ import { turtle } from "codemirror-lang-turtle"; import { javascript } from "@codemirror/legacy-modes/mode/javascript"; import { addClass, removeClass } from "@matdata/yasgui-utils"; import { DeepReadonly } from "ts-essentials"; -import { extractUriAtOffset, buildSubjectOfQuery, buildObjectOfQuery } from "./uriUtils"; +import { + extractUriAtOffset, + buildSubjectOfQuery, + buildObjectOfQuery, + stripDuplicatePrefixDeclarations, +} from "./uriUtils"; export interface PluginConfig { maxLines: number; @@ -27,7 +32,7 @@ export default class Response implements Plugin { private config: DeepReadonly; private overLay: HTMLDivElement | undefined; private cm: EditorView | undefined; - /** Turtle appended to the response view through Ctrl+Click DESCRIBE actions. */ + /** Turtle appended to the response view through Ctrl+Click URI exploration actions. */ private appendedContent = ""; constructor(yasr: Yasr) { this.yasr = yasr; @@ -213,8 +218,8 @@ export default class Response implements Plugin { } /** - * Pick an Accept header for the DESCRIBE request. When the current response is an - * RDF graph format, reuse it so the appended triples match what is already shown. + * Pick an Accept header for follow-up CONSTRUCT requests. When the current response + * is an RDF graph format, reuse it so the appended triples match what is shown. */ private getConstructAcceptHeader(): string { const contentType = this.yasr.results?.getContentType(); @@ -244,12 +249,13 @@ export default class Response implements Plugin { } /** - * Append text (a DESCRIBE result or a status message) to the response view and - * make sure the full content is revealed. + * Append text (query result or status message) to the response view and make sure + * the full content is revealed. */ private appendToView(text: string) { if (!this.cm || !text) return; - this.appendedContent += text; + const existingVisibleContent = (this.yasr.results?.getOriginalResponseAsString() || "") + this.appendedContent; + this.appendedContent += stripDuplicatePrefixDeclarations(text, existingVisibleContent); // Reveal the full response together with the appended DESCRIBE results. this.showMore(); } diff --git a/packages/yasr/src/plugins/response/uriUtils.ts b/packages/yasr/src/plugins/response/uriUtils.ts index ea5e73d5..5a72bf5d 100644 --- a/packages/yasr/src/plugins/response/uriUtils.ts +++ b/packages/yasr/src/plugins/response/uriUtils.ts @@ -9,6 +9,8 @@ const ANGLE_IRI_RE = /<([^<>\s"{}|\\^`]+)>/g; // Matches bare or quoted URIs as they appear in JSON / XML / CSV responses, e.g. `"http://example.org/foo"` const BARE_URI_RE = /(?:https?|urn|ftp|mailto):[^\s<>"'`{}|\\^[\]]+/g; +// Matches Turtle/SPARQL prefix declarations such as `@prefix ex: <...> .` and `PREFIX ex: <...>`. +const PREFIX_DECL_RE = /^\s*(?:@prefix|PREFIX)\s+(\w*):\s*<[^>]+>\s*\.?\s*$/i; /** * Remove trailing punctuation that is commonly adjacent to a URI in a serialized @@ -75,3 +77,41 @@ export function buildSubjectOfQuery(uri: string): string { export function buildObjectOfQuery(uri: string): string { return `CONSTRUCT { ?s ?p <${uri}> } WHERE { ?s ?p <${uri}> } LIMIT 1000`; } + +/** + * Remove duplicate prefix declarations from a text block. + * + * Prefixes are considered duplicates by label (case-insensitive), and can be + * deduplicated against already-rendered content in the response viewer. + */ +export function stripDuplicatePrefixDeclarations(text: string, existingText = ""): string { + if (!text) return text; + + const seenLabels = new Set(); + const addSeenLabels = (source: string) => { + for (const line of source.split("\n")) { + const match = line.match(PREFIX_DECL_RE); + if (!match) continue; + seenLabels.add(match[1].toLowerCase()); + } + }; + + addSeenLabels(existingText); + + const deduplicatedLines: string[] = []; + for (const line of text.split("\n")) { + const match = line.match(PREFIX_DECL_RE); + if (!match) { + deduplicatedLines.push(line); + continue; + } + + const label = match[1].toLowerCase(); + if (seenLabels.has(label)) continue; + + seenLabels.add(label); + deduplicatedLines.push(line); + } + + return deduplicatedLines.join("\n"); +} diff --git a/test/unit/response-uri-utils-test.ts b/test/unit/response-uri-utils-test.ts index a23d6226..70577605 100644 --- a/test/unit/response-uri-utils-test.ts +++ b/test/unit/response-uri-utils-test.ts @@ -5,6 +5,7 @@ import { extractUriAtOffset, buildSubjectOfQuery, buildObjectOfQuery, + stripDuplicatePrefixDeclarations, } from "../../packages/yasr/src/plugins/response/uriUtils.js"; const expect = chai.expect; @@ -71,4 +72,28 @@ describe("Response plugin URI utilities", () => { ); }); }); + + describe("stripDuplicatePrefixDeclarations", () => { + it("removes appended prefixes that already exist in the current view", () => { + const existing = "@prefix ex: .\n ."; + const appended = + "@prefix ex: .\n@prefix foaf: .\n a foaf:Person ."; + expect(stripDuplicatePrefixDeclarations(appended, existing)).to.equal( + "@prefix foaf: .\n a foaf:Person .", + ); + }); + + it("deduplicates duplicate prefix labels within the appended block", () => { + const appended = + "PREFIX ex: \nPREFIX EX: \n ."; + expect(stripDuplicatePrefixDeclarations(appended)).to.equal( + "PREFIX ex: \n .", + ); + }); + + it("keeps non-prefix lines unchanged", () => { + const appended = "# Triples where is subject\n ."; + expect(stripDuplicatePrefixDeclarations(appended)).to.equal(appended); + }); + }); });