@matdata/yasr 5.3.0 → 5.4.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.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@matdata/yasr",
3
3
  "description": "Yet Another SPARQL Resultset GUI",
4
- "version": "5.3.0",
4
+ "version": "5.4.0",
5
5
  "main": "build/yasr.min.js",
6
6
  "types": "build/ts/src/index.d.ts",
7
7
  "license": "MIT",
@@ -227,10 +227,9 @@ const getHtml = (plugin) => `
227
227
  <script src="https://cdnjs.cloudflare.com/ajax/libs/babel-polyfill/7.0.0/polyfill.js"></script>
228
228
  <style>
229
229
  body {
230
- font-family: 'Roboto', sans-serif;
230
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif;
231
231
  }
232
232
  </style>
233
- <link href="https://fonts.googleapis.com/css?family=Roboto" rel="stylesheet" />
234
233
  <link rel="stylesheet" href="build/yasr.min.css" />
235
234
  <link rel="stylesheet" href="build/pro-gallery.min.css">
236
235
  <link rel="stylesheet" href="build/pro-geo.min.css">
@@ -302,7 +301,7 @@ const getScreenWidth = (plugin) => {
302
301
  };
303
302
 
304
303
  let staticFileServer = new static.Server("./");
305
- function setupServer () {
304
+ function setupServer() {
306
305
  return new Promise((resolve, reject) => {
307
306
  var server = http
308
307
  .createServer(function (request, response) {
@@ -319,10 +318,10 @@ function setupServer () {
319
318
  .on("error", (e) => reject(e));
320
319
  });
321
320
  }
322
- function wait (time) {
321
+ function wait(time) {
323
322
  return new Promise((resolve) => setTimeout(resolve, time));
324
323
  }
325
- function waitForImagesToLoad (page) {
324
+ function waitForImagesToLoad(page) {
326
325
  return page.evaluate(() => {
327
326
  const selectors = Array.from(document.querySelectorAll("img"));
328
327
  return Promise.all(
@@ -332,7 +331,7 @@ function waitForImagesToLoad (page) {
332
331
  img.addEventListener("load", resolve);
333
332
  img.addEventListener("error", reject);
334
333
  });
335
- })
334
+ }),
336
335
  );
337
336
  });
338
337
  }
@@ -0,0 +1,246 @@
1
+ import { describe, it } from "mocha";
2
+ import { expect } from "chai";
3
+ import Yasr from "../../../index";
4
+ import Parser from "../../../parsers";
5
+
6
+ describe("Table Plugin", () => {
7
+ describe("Markdown Generation", () => {
8
+ it("should generate markdown table from simple results", () => {
9
+ const container = document.createElement("div");
10
+ const yasr = new Yasr(container);
11
+
12
+ // Mock SPARQL results
13
+ const mockResults = {
14
+ head: {
15
+ vars: ["subject", "predicate", "object"],
16
+ },
17
+ results: {
18
+ bindings: [
19
+ {
20
+ subject: { type: "uri", value: "http://example.org/resource/1" } as Parser.BindingValue,
21
+ predicate: { type: "uri", value: "http://example.org/name" } as Parser.BindingValue,
22
+ object: { type: "literal", value: "Test Name" } as Parser.BindingValue,
23
+ },
24
+ {
25
+ subject: { type: "uri", value: "http://example.org/resource/2" } as Parser.BindingValue,
26
+ predicate: { type: "uri", value: "http://example.org/name" } as Parser.BindingValue,
27
+ object: { type: "literal", value: "Another Name" } as Parser.BindingValue,
28
+ },
29
+ ],
30
+ },
31
+ };
32
+
33
+ yasr.setResponse(
34
+ {
35
+ response: JSON.stringify(mockResults),
36
+ status: 200,
37
+ contentType: "application/sparql-results+json",
38
+ },
39
+ 0,
40
+ );
41
+
42
+ const tablePlugin = yasr.plugins["table"];
43
+ // @ts-ignore - accessing private method for testing
44
+ const markdown = tablePlugin.getMarkdownTable();
45
+
46
+ expect(markdown).to.be.a("string");
47
+ expect(markdown).to.include("| subject | predicate | object |");
48
+ expect(markdown).to.include("| --- | --- | --- |");
49
+ expect(markdown).to.include("http://example.org/resource/1");
50
+ expect(markdown).to.include("Test Name");
51
+ });
52
+
53
+ it("should escape pipe characters in markdown values", () => {
54
+ const container = document.createElement("div");
55
+ const yasr = new Yasr(container);
56
+
57
+ // Mock SPARQL results with pipe character
58
+ const mockResults = {
59
+ head: {
60
+ vars: ["value"],
61
+ },
62
+ results: {
63
+ bindings: [
64
+ {
65
+ value: { type: "literal", value: "text|with|pipes" } as Parser.BindingValue,
66
+ },
67
+ ],
68
+ },
69
+ };
70
+
71
+ yasr.setResponse(
72
+ {
73
+ response: JSON.stringify(mockResults),
74
+ status: 200,
75
+ contentType: "application/sparql-results+json",
76
+ },
77
+ 0,
78
+ );
79
+
80
+ const tablePlugin = yasr.plugins["table"];
81
+ // @ts-ignore - accessing private method for testing
82
+ const markdown = tablePlugin.getMarkdownTable();
83
+
84
+ expect(markdown).to.include("text\\|with\\|pipes");
85
+ });
86
+ });
87
+
88
+ describe("URI Prefixing", () => {
89
+ it("should show URI prefixes when enabled", () => {
90
+ const container = document.createElement("div");
91
+ const yasr = new Yasr(container, {
92
+ prefixes: {
93
+ ex: "http://example.org/",
94
+ },
95
+ });
96
+
97
+ const mockResults = {
98
+ head: {
99
+ vars: ["resource"],
100
+ },
101
+ results: {
102
+ bindings: [
103
+ {
104
+ resource: { type: "uri", value: "http://example.org/test" } as Parser.BindingValue,
105
+ },
106
+ ],
107
+ },
108
+ };
109
+
110
+ yasr.setResponse(
111
+ {
112
+ response: JSON.stringify(mockResults),
113
+ status: 200,
114
+ contentType: "application/sparql-results+json",
115
+ },
116
+ 0,
117
+ );
118
+
119
+ const tablePlugin = yasr.plugins["table"];
120
+ // Draw with prefixes enabled (default)
121
+ tablePlugin.draw({ showUriPrefixes: true });
122
+
123
+ const tableHtml = container.innerHTML;
124
+ expect(tableHtml).to.include("ex:test");
125
+ });
126
+
127
+ it("should show full URIs when prefixing disabled", () => {
128
+ const container = document.createElement("div");
129
+ const yasr = new Yasr(container, {
130
+ prefixes: {
131
+ ex: "http://example.org/",
132
+ },
133
+ });
134
+
135
+ const mockResults = {
136
+ head: {
137
+ vars: ["resource"],
138
+ },
139
+ results: {
140
+ bindings: [
141
+ {
142
+ resource: { type: "uri", value: "http://example.org/test" } as Parser.BindingValue,
143
+ },
144
+ ],
145
+ },
146
+ };
147
+
148
+ yasr.setResponse(
149
+ {
150
+ response: JSON.stringify(mockResults),
151
+ status: 200,
152
+ contentType: "application/sparql-results+json",
153
+ },
154
+ 0,
155
+ );
156
+
157
+ const tablePlugin = yasr.plugins["table"];
158
+ // Draw with prefixes disabled
159
+ tablePlugin.draw({ showUriPrefixes: false });
160
+
161
+ const tableHtml = container.innerHTML;
162
+ expect(tableHtml).to.include("http://example.org/test");
163
+ expect(tableHtml).to.not.include("ex:test");
164
+ });
165
+ });
166
+
167
+ describe("Datatype Display", () => {
168
+ it("should show datatypes when enabled", () => {
169
+ const container = document.createElement("div");
170
+ const yasr = new Yasr(container);
171
+
172
+ const mockResults = {
173
+ head: {
174
+ vars: ["number"],
175
+ },
176
+ results: {
177
+ bindings: [
178
+ {
179
+ number: {
180
+ type: "literal",
181
+ value: "42",
182
+ datatype: "http://www.w3.org/2001/XMLSchema#integer",
183
+ } as Parser.BindingValue,
184
+ },
185
+ ],
186
+ },
187
+ };
188
+
189
+ yasr.setResponse(
190
+ {
191
+ response: JSON.stringify(mockResults),
192
+ status: 200,
193
+ contentType: "application/sparql-results+json",
194
+ },
195
+ 0,
196
+ );
197
+
198
+ const tablePlugin = yasr.plugins["table"];
199
+ // Draw with datatypes enabled (default)
200
+ tablePlugin.draw({ showDatatypes: true });
201
+
202
+ const tableHtml = container.innerHTML;
203
+ expect(tableHtml).to.include("^^");
204
+ expect(tableHtml).to.include("XMLSchema#integer");
205
+ });
206
+
207
+ it("should hide datatypes when disabled", () => {
208
+ const container = document.createElement("div");
209
+ const yasr = new Yasr(container);
210
+
211
+ const mockResults = {
212
+ head: {
213
+ vars: ["number"],
214
+ },
215
+ results: {
216
+ bindings: [
217
+ {
218
+ number: {
219
+ type: "literal",
220
+ value: "42",
221
+ datatype: "http://www.w3.org/2001/XMLSchema#integer",
222
+ } as Parser.BindingValue,
223
+ },
224
+ ],
225
+ },
226
+ };
227
+
228
+ yasr.setResponse(
229
+ {
230
+ response: JSON.stringify(mockResults),
231
+ status: 200,
232
+ contentType: "application/sparql-results+json",
233
+ },
234
+ 0,
235
+ );
236
+
237
+ const tablePlugin = yasr.plugins["table"];
238
+ // Draw with datatypes disabled
239
+ tablePlugin.draw({ showDatatypes: false });
240
+
241
+ const tableHtml = container.innerHTML;
242
+ expect(tableHtml).to.not.include("^^");
243
+ expect(tableHtml).to.not.include("XMLSchema#integer");
244
+ });
245
+ });
246
+ });
@@ -28,15 +28,34 @@ Delimiters used by datatables
28
28
  align-items: center;
29
29
  margin-right: 10px;
30
30
  }
31
+ .copyMarkdownBtn {
32
+ margin-right: 10px;
33
+ padding: 4px 8px;
34
+ cursor: pointer;
35
+ border: 1px solid var(--yasgui-border-color, #ddd);
36
+ background-color: var(--yasgui-bg-secondary, #f7f7f7);
37
+ color: var(--yasgui-text-primary, #000);
38
+ border-radius: 3px;
39
+ font-size: 0.9em;
40
+ white-space: nowrap;
41
+
42
+ &:hover {
43
+ background-color: var(--yasgui-bg-tertiary, #eee);
44
+ }
45
+
46
+ &:active {
47
+ background-color: var(--yasgui-bg-primary, #fff);
48
+ }
49
+ }
31
50
  }
32
- .dataTable.ellipseTable {
51
+ .dataTable.compactTable {
33
52
  white-space: nowrap;
34
53
  div:not(.expanded) {
35
54
  overflow: hidden;
36
55
  text-overflow: ellipsis;
37
56
  }
38
57
  }
39
- .dataTable:not(.ellipseTable) {
58
+ .dataTable:not(.compactTable) {
40
59
  div:not(.expanded) {
41
60
  word-break: break-all;
42
61
  }
@@ -52,7 +71,7 @@ Delimiters used by datatables
52
71
  }
53
72
  }
54
73
 
55
- .dataTables_wrapper {
74
+ .dt-container {
56
75
  font-size: 0.9em;
57
76
  min-width: 100%;
58
77
  .grip-container {
@@ -72,7 +91,7 @@ Delimiters used by datatables
72
91
  }
73
92
 
74
93
  tbody tr:hover {
75
- background-color: #f9f9f9;
94
+ background-color: var(--yasgui-bg-tertiary, #f9f9f9);
76
95
  }
77
96
 
78
97
  thead tr th {
@@ -92,7 +111,7 @@ Delimiters used by datatables
92
111
  padding-right: 18px; //space for sort icon
93
112
  }
94
113
  &:hover {
95
- background-color: #f9f9f9;
114
+ background-color: var(--yasgui-tab-bg, #f9f9f9);
96
115
  }
97
116
  }
98
117
 
@@ -119,35 +138,29 @@ Delimiters used by datatables
119
138
  /**
120
139
  Selector for pagination element
121
140
  */
122
- div.dataTables_paginate.paging_simple_numbers {
123
- a.paginate_button {
141
+ div.dt-paging {
142
+ button.dt-paging-button {
124
143
  border: none;
125
144
  background: transparent;
145
+ color: var(--yasgui-text-secondary, #505050);
126
146
 
127
147
  // When the buttons are disabled show the default YASR disabled color
128
- .disabled {
129
- color: #505050;
148
+ &.disabled {
149
+ color: var(--yasgui-text-muted, #999) !important;
130
150
  }
131
151
 
132
152
  &.current {
133
- border: none;
134
- background: transparent;
135
153
  text-decoration: underline !important; // dataTables made the text-decoration important
154
+ color: var(--yasgui-accent-color, #337ab7);
136
155
  }
137
156
 
138
157
  &:hover {
139
- border: none;
140
- background: transparent;
141
-
142
158
  // Don't override the disabled grayed out style
143
159
  &:not(.disabled) {
144
- color: black !important; // dataTables made the color important
160
+ font-weight: bold;
161
+ color: var(--yasgui-text-secondary, #505050) !important;
145
162
  }
146
163
  }
147
-
148
- &:active {
149
- box-shadow: none;
150
- }
151
164
  }
152
165
  }
153
166
  }
@@ -28,7 +28,9 @@ export interface PluginConfig {
28
28
  export interface PersistentConfig {
29
29
  pageSize?: number;
30
30
  compact?: boolean;
31
- isEllipsed?: boolean;
31
+ isCompactView?: boolean;
32
+ showUriPrefixes?: boolean;
33
+ showDatatypes?: boolean;
32
34
  }
33
35
 
34
36
  type DataRow = [number, ...(Parser.BindingValue | "")[]];
@@ -48,7 +50,9 @@ export default class Table implements Plugin<PluginConfig> {
48
50
  private tableFilterField: HTMLInputElement | undefined;
49
51
  private tableSizeField: HTMLSelectElement | undefined;
50
52
  private tableCompactSwitch: HTMLInputElement | undefined;
51
- private tableEllipseSwitch: HTMLInputElement | undefined;
53
+ private tableCompactViewSwitch: HTMLInputElement | undefined;
54
+ private tableUriPrefixSwitch: HTMLInputElement | undefined;
55
+ private tableDatatypeSwitch: HTMLInputElement | undefined;
52
56
  private tableResizer:
53
57
  | {
54
58
  reset: (options: {
@@ -109,11 +113,40 @@ export default class Table implements Plugin<PluginConfig> {
109
113
  return bindings.map((binding, rowId) => [rowId + 1, ...vars.map((variable) => binding[variable] ?? "")]);
110
114
  }
111
115
 
116
+ private getMarkdownTable(): string {
117
+ if (!this.yasr.results) return "";
118
+ const bindings = this.yasr.results.getBindings();
119
+ if (!bindings) return "";
120
+ const vars = this.yasr.results.getVariables();
121
+
122
+ // Helper to escape special characters in markdown (backslashes and pipes)
123
+ const escapeMarkdown = (str: string) => str.replace(/\\/g, "\\\\").replace(/\|/g, "\\|");
124
+
125
+ // Helper to get plain text value from binding
126
+ const getPlainValue = (binding: Parser.BindingValue | ""): string => {
127
+ if (binding === "") return "";
128
+ return escapeMarkdown(binding.value);
129
+ };
130
+
131
+ // Create header row
132
+ let markdown = "| " + vars.map(escapeMarkdown).join(" | ") + " |\n";
133
+ // Create separator row
134
+ markdown += "| " + vars.map(() => "---").join(" | ") + " |\n";
135
+ // Create data rows
136
+ bindings.forEach((binding) => {
137
+ const row = vars.map((variable) => getPlainValue(binding[variable] ?? ""));
138
+ markdown += "| " + row.join(" | ") + " |\n";
139
+ });
140
+
141
+ return markdown;
142
+ }
143
+
112
144
  private getUriLinkFromBinding(binding: Parser.BindingValue, prefixes?: { [key: string]: string }) {
113
145
  const href = sanitize(binding.value);
114
146
  let visibleString = href;
115
147
  let prefixed = false;
116
- if (prefixes) {
148
+ // Apply URI prefixing if enabled (default true)
149
+ if (this.persistentConfig.showUriPrefixes !== false && prefixes) {
117
150
  for (const prefixLabel in prefixes) {
118
151
  if (visibleString.indexOf(prefixes[prefixLabel]) == 0) {
119
152
  visibleString = prefixLabel + ":" + href.substring(prefixes[prefixLabel].length);
@@ -146,11 +179,14 @@ export default class Table implements Plugin<PluginConfig> {
146
179
  // Return now when in compact mode.
147
180
  if (this.persistentConfig.compact) return stringRepresentation;
148
181
 
149
- if (literalBinding["xml:lang"]) {
150
- stringRepresentation = `"${stringRepresentation}"<sup>@${literalBinding["xml:lang"]}</sup>`;
151
- } else if (literalBinding.datatype) {
152
- const dataType = this.getUriLinkFromBinding({ type: "uri", value: literalBinding.datatype }, prefixes);
153
- stringRepresentation = `"${stringRepresentation}"<sup>^^${dataType}</sup>`;
182
+ // Show datatypes if enabled (default true)
183
+ if (this.persistentConfig.showDatatypes !== false) {
184
+ if (literalBinding["xml:lang"]) {
185
+ stringRepresentation = `"${stringRepresentation}"<sup>@${literalBinding["xml:lang"]}</sup>`;
186
+ } else if (literalBinding.datatype) {
187
+ const dataType = this.getUriLinkFromBinding({ type: "uri", value: literalBinding.datatype }, prefixes);
188
+ stringRepresentation = `"${stringRepresentation}"<sup>^^${dataType}</sup>`;
189
+ }
154
190
  }
155
191
  return stringRepresentation;
156
192
  }
@@ -223,15 +259,15 @@ export default class Table implements Plugin<PluginConfig> {
223
259
  this.tableResizer = new ColumnResizer(this.tableEl, {
224
260
  widths: this.persistentConfig.compact === true ? widths : [this.getSizeFirstColumn(), ...widths.slice(1)],
225
261
  partialRefresh: true,
226
- onResize: this.persistentConfig.isEllipsed !== false && this.setEllipsisHandlers,
262
+ onResize: this.persistentConfig.isCompactView !== false && this.setEllipsisHandlers,
227
263
  headerOnly: true,
228
264
  });
229
265
  // DataTables uses the rendered style to decide the widths of columns.
230
- // Before a draw remove the ellipseTable styling
231
- if (this.persistentConfig.isEllipsed !== false) {
266
+ // Before a draw remove the compactTable styling
267
+ if (this.persistentConfig.isCompactView !== false) {
232
268
  this.dataTable?.on("preDraw", () => {
233
269
  this.tableResizer?.reset({ disable: true });
234
- removeClass(this.tableEl, "ellipseTable");
270
+ removeClass(this.tableEl, "compactTable");
235
271
  this.tableEl?.style.removeProperty("width");
236
272
  this.tableEl?.style.setProperty("width", this.tableEl.clientWidth + "px");
237
273
  return true; // Indicate it should re-render
@@ -252,8 +288,8 @@ export default class Table implements Plugin<PluginConfig> {
252
288
  onResize: this.setEllipsisHandlers,
253
289
  headerOnly: true,
254
290
  });
255
- // Re-add the ellipsis
256
- addClass(this.tableEl, "ellipseTable");
291
+ // Re-add the compact styling
292
+ addClass(this.tableEl, "compactTable");
257
293
  // Check if cells need the ellipsisHandlers
258
294
  this.setEllipsisHandlers();
259
295
  });
@@ -261,8 +297,8 @@ export default class Table implements Plugin<PluginConfig> {
261
297
 
262
298
  this.drawControls();
263
299
  // Draw again but with the events
264
- if (this.persistentConfig.isEllipsed !== false) {
265
- addClass(this.tableEl, "ellipseTable");
300
+ if (this.persistentConfig.isCompactView !== false) {
301
+ addClass(this.tableEl, "compactTable");
266
302
  this.setEllipsisHandlers();
267
303
  }
268
304
  // if (this.tableEl.clientWidth > width) this.tableEl.parentElement?.style.setProperty("overflow", "hidden");
@@ -307,13 +343,53 @@ export default class Table implements Plugin<PluginConfig> {
307
343
  this.draw(this.persistentConfig);
308
344
  this.yasr.storePluginConfig("table", this.persistentConfig);
309
345
  };
310
- private handleSetEllipsisToggle = (event: Event) => {
346
+ private handleSetCompactViewToggle = (event: Event) => {
347
+ // Store in persistentConfig
348
+ this.persistentConfig.isCompactView = (event.target as HTMLInputElement).checked;
349
+ // Update the table
350
+ this.draw(this.persistentConfig);
351
+ this.yasr.storePluginConfig("table", this.persistentConfig);
352
+ };
353
+ private handleSetUriPrefixToggle = (event: Event) => {
354
+ // Store in persistentConfig
355
+ this.persistentConfig.showUriPrefixes = (event.target as HTMLInputElement).checked;
356
+ // Update the table
357
+ this.draw(this.persistentConfig);
358
+ this.yasr.storePluginConfig("table", this.persistentConfig);
359
+ };
360
+ private handleSetDatatypeToggle = (event: Event) => {
311
361
  // Store in persistentConfig
312
- this.persistentConfig.isEllipsed = (event.target as HTMLInputElement).checked;
362
+ this.persistentConfig.showDatatypes = (event.target as HTMLInputElement).checked;
313
363
  // Update the table
314
364
  this.draw(this.persistentConfig);
315
365
  this.yasr.storePluginConfig("table", this.persistentConfig);
316
366
  };
367
+ private handleCopyMarkdown = async (event: Event) => {
368
+ const markdown = this.getMarkdownTable();
369
+ const button = event.target as HTMLButtonElement;
370
+
371
+ // Prevent multiple rapid clicks
372
+ if (button.disabled) return;
373
+ button.disabled = true;
374
+
375
+ const originalText = "Copy as Markdown";
376
+ try {
377
+ await navigator.clipboard.writeText(markdown);
378
+ // Provide visual feedback
379
+ button.textContent = "Copied!";
380
+ setTimeout(() => {
381
+ button.textContent = originalText;
382
+ button.disabled = false;
383
+ }, 2000);
384
+ } catch (err) {
385
+ // Show user-friendly error
386
+ button.textContent = "Copy failed";
387
+ setTimeout(() => {
388
+ button.textContent = originalText;
389
+ button.disabled = false;
390
+ }, 2000);
391
+ }
392
+ };
317
393
  /**
318
394
  * Draws controls on each update
319
395
  */
@@ -339,21 +415,53 @@ export default class Table implements Plugin<PluginConfig> {
339
415
  this.tableCompactSwitch.defaultChecked = !!this.persistentConfig.compact;
340
416
  this.tableControls.appendChild(toggleWrapper);
341
417
 
342
- // Ellipsis switch
343
- const ellipseToggleWrapper = document.createElement("div");
344
- const ellipseSwitchComponent = document.createElement("label");
345
- const ellipseTextComponent = document.createElement("span");
346
- ellipseTextComponent.innerText = "Ellipse";
347
- addClass(ellipseTextComponent, "label");
348
- ellipseSwitchComponent.appendChild(ellipseTextComponent);
349
- addClass(ellipseSwitchComponent, "switch");
350
- ellipseToggleWrapper.appendChild(ellipseSwitchComponent);
351
- this.tableEllipseSwitch = document.createElement("input");
352
- ellipseSwitchComponent.addEventListener("change", this.handleSetEllipsisToggle);
353
- this.tableEllipseSwitch.type = "checkbox";
354
- ellipseSwitchComponent.appendChild(this.tableEllipseSwitch);
355
- this.tableEllipseSwitch.defaultChecked = this.persistentConfig.isEllipsed !== false;
356
- this.tableControls.appendChild(ellipseToggleWrapper);
418
+ // Compact view switch
419
+ const compactViewToggleWrapper = document.createElement("div");
420
+ const compactViewSwitchComponent = document.createElement("label");
421
+ const compactViewTextComponent = document.createElement("span");
422
+ compactViewTextComponent.innerText = "Compact";
423
+ addClass(compactViewTextComponent, "label");
424
+ compactViewSwitchComponent.appendChild(compactViewTextComponent);
425
+ addClass(compactViewSwitchComponent, "switch");
426
+ compactViewToggleWrapper.appendChild(compactViewSwitchComponent);
427
+ this.tableCompactViewSwitch = document.createElement("input");
428
+ compactViewSwitchComponent.addEventListener("change", this.handleSetCompactViewToggle);
429
+ this.tableCompactViewSwitch.type = "checkbox";
430
+ compactViewSwitchComponent.appendChild(this.tableCompactViewSwitch);
431
+ this.tableCompactViewSwitch.defaultChecked = this.persistentConfig.isCompactView !== false;
432
+ this.tableControls.appendChild(compactViewToggleWrapper);
433
+
434
+ // URI Prefix switch
435
+ const uriPrefixToggleWrapper = document.createElement("div");
436
+ const uriPrefixSwitchComponent = document.createElement("label");
437
+ const uriPrefixTextComponent = document.createElement("span");
438
+ uriPrefixTextComponent.innerText = "Prefixes";
439
+ addClass(uriPrefixTextComponent, "label");
440
+ uriPrefixSwitchComponent.appendChild(uriPrefixTextComponent);
441
+ addClass(uriPrefixSwitchComponent, "switch");
442
+ uriPrefixToggleWrapper.appendChild(uriPrefixSwitchComponent);
443
+ this.tableUriPrefixSwitch = document.createElement("input");
444
+ uriPrefixSwitchComponent.addEventListener("change", this.handleSetUriPrefixToggle);
445
+ this.tableUriPrefixSwitch.type = "checkbox";
446
+ uriPrefixSwitchComponent.appendChild(this.tableUriPrefixSwitch);
447
+ this.tableUriPrefixSwitch.defaultChecked = this.persistentConfig.showUriPrefixes !== false;
448
+ this.tableControls.appendChild(uriPrefixToggleWrapper);
449
+
450
+ // Datatype switch
451
+ const datatypeToggleWrapper = document.createElement("div");
452
+ const datatypeSwitchComponent = document.createElement("label");
453
+ const datatypeTextComponent = document.createElement("span");
454
+ datatypeTextComponent.innerText = "Datatypes";
455
+ addClass(datatypeTextComponent, "label");
456
+ datatypeSwitchComponent.appendChild(datatypeTextComponent);
457
+ addClass(datatypeSwitchComponent, "switch");
458
+ datatypeToggleWrapper.appendChild(datatypeSwitchComponent);
459
+ this.tableDatatypeSwitch = document.createElement("input");
460
+ datatypeSwitchComponent.addEventListener("change", this.handleSetDatatypeToggle);
461
+ this.tableDatatypeSwitch.type = "checkbox";
462
+ datatypeSwitchComponent.appendChild(this.tableDatatypeSwitch);
463
+ this.tableDatatypeSwitch.defaultChecked = this.persistentConfig.showDatatypes !== false;
464
+ this.tableControls.appendChild(datatypeToggleWrapper);
357
465
 
358
466
  // Create table filter
359
467
  this.tableFilterField = document.createElement("input");
@@ -363,6 +471,14 @@ export default class Table implements Plugin<PluginConfig> {
363
471
  this.tableControls.appendChild(this.tableFilterField);
364
472
  this.tableFilterField.addEventListener("keyup", this.handleTableSearch);
365
473
 
474
+ // Create markdown copy button
475
+ const markdownButton = document.createElement("button");
476
+ markdownButton.className = "copyMarkdownBtn";
477
+ markdownButton.textContent = "Copy as Markdown";
478
+ markdownButton.setAttribute("aria-label", "Copy table as markdown");
479
+ markdownButton.addEventListener("click", this.handleCopyMarkdown);
480
+ this.tableControls.appendChild(markdownButton);
481
+
366
482
  // Create page wrapper
367
483
  const pageSizerWrapper = document.createElement("div");
368
484
  pageSizerWrapper.className = "pageSizeWrapper";
@@ -413,8 +529,12 @@ export default class Table implements Plugin<PluginConfig> {
413
529
  this.tableSizeField = undefined;
414
530
  this.tableCompactSwitch?.removeEventListener("change", this.handleSetCompactToggle);
415
531
  this.tableCompactSwitch = undefined;
416
- this.tableEllipseSwitch?.removeEventListener("change", this.handleSetEllipsisToggle);
417
- this.tableEllipseSwitch = undefined;
532
+ this.tableCompactViewSwitch?.removeEventListener("change", this.handleSetCompactViewToggle);
533
+ this.tableCompactViewSwitch = undefined;
534
+ this.tableUriPrefixSwitch?.removeEventListener("change", this.handleSetUriPrefixToggle);
535
+ this.tableUriPrefixSwitch = undefined;
536
+ this.tableDatatypeSwitch?.removeEventListener("change", this.handleSetDatatypeToggle);
537
+ this.tableDatatypeSwitch = undefined;
418
538
  // Empty controls
419
539
  while (this.tableControls?.firstChild) this.tableControls.firstChild.remove();
420
540
  this.tableControls?.remove();