@matdata/yasqe 5.3.0 → 5.5.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/yasqe",
3
3
  "description": "Yet Another SPARQL Query Editor",
4
- "version": "5.3.0",
4
+ "version": "5.5.0",
5
5
  "main": "build/yasqe.min.js",
6
6
  "types": "build/ts/src/index.d.ts",
7
7
  "license": "MIT",
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Basic Authentication Tests
3
+ */
4
+
5
+ import { describe, it } from "mocha";
6
+ import { expect } from "chai";
7
+
8
+ describe("Basic Authentication", () => {
9
+ describe("Base64 Encoding", () => {
10
+ it("should encode credentials correctly", () => {
11
+ const username = "testuser";
12
+ const password = "testpass";
13
+ const credentials = `${username}:${password}`;
14
+ const encoded = btoa(credentials);
15
+ const expected = "dGVzdHVzZXI6dGVzdHBhc3M=";
16
+
17
+ expect(encoded).to.equal(expected);
18
+ });
19
+
20
+ it("should handle special characters", () => {
21
+ const username = "user@example.com";
22
+ const password = "p@ss:word!";
23
+ const credentials = `${username}:${password}`;
24
+ const encoded = btoa(credentials);
25
+
26
+ // Verify it can be decoded back
27
+ const decoded = atob(encoded);
28
+ expect(decoded).to.equal(credentials);
29
+ });
30
+ });
31
+
32
+ describe("Authorization Header Format", () => {
33
+ it("should create proper Basic auth header", () => {
34
+ const username = "admin";
35
+ const password = "secret";
36
+ const credentials = `${username}:${password}`;
37
+ const encoded = btoa(credentials);
38
+ const header = `Basic ${encoded}`;
39
+
40
+ expect(header).to.equal("Basic YWRtaW46c2VjcmV0");
41
+ expect(header).to.match(/^Basic [A-Za-z0-9+/=]+$/);
42
+ });
43
+ });
44
+
45
+ describe("Empty Credentials", () => {
46
+ it("should handle empty username", () => {
47
+ const username = "";
48
+ const password = "password";
49
+ const credentials = `${username}:${password}`;
50
+ const encoded = btoa(credentials);
51
+
52
+ expect(encoded).to.be.a("string");
53
+ expect(encoded.length).to.be.greaterThan(0);
54
+ });
55
+
56
+ it("should handle empty password", () => {
57
+ const username = "user";
58
+ const password = "";
59
+ const credentials = `${username}:${password}`;
60
+ const encoded = btoa(credentials);
61
+
62
+ expect(encoded).to.be.a("string");
63
+ expect(encoded.length).to.be.greaterThan(0);
64
+ });
65
+ });
66
+ });
@@ -17,8 +17,7 @@
17
17
 
18
18
  background: white;
19
19
  font-size: 90%;
20
- font-family: monospace;
21
-
20
+ font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier, monospace;
22
21
  max-height: 20em;
23
22
  overflow-y: auto;
24
23
  }
package/src/defaults.ts CHANGED
@@ -132,6 +132,34 @@ SELECT * WHERE {
132
132
  prefixCcApi: prefixCcApi,
133
133
  showFormatButton: true,
134
134
  checkConstructVariables: true,
135
+ snippets: [
136
+ {
137
+ label: "SELECT",
138
+ code: "SELECT * WHERE {\n ?s ?p ?o .\n} LIMIT 10",
139
+ group: "Query Types",
140
+ },
141
+ {
142
+ label: "CONSTRUCT",
143
+ code: "CONSTRUCT {\n ?s ?p ?o .\n} WHERE {\n ?s ?p ?o .\n} LIMIT 10",
144
+ group: "Query Types",
145
+ },
146
+ {
147
+ label: "ASK",
148
+ code: "ASK {\n ?s ?p ?o .\n}",
149
+ group: "Query Types",
150
+ },
151
+ {
152
+ label: "FILTER",
153
+ code: "FILTER (?var > 100)",
154
+ group: "Patterns",
155
+ },
156
+ {
157
+ label: "OPTIONAL",
158
+ code: "OPTIONAL {\n ?s ?p ?o .\n}",
159
+ group: "Patterns",
160
+ },
161
+ ],
162
+ showSnippetsBar: true,
135
163
  };
136
164
  const requestConfig: PlainRequestConfig = {
137
165
  queryArgument: undefined, //undefined means: get query argument based on query mode
@@ -146,6 +174,7 @@ SELECT * WHERE {
146
174
  headers: {},
147
175
  withCredentials: false,
148
176
  adjustQueryBeforeRequest: false,
177
+ basicAuth: undefined,
149
178
  };
150
179
  return { ...config, requestConfig };
151
180
  }
package/src/imgs.ts CHANGED
@@ -14,3 +14,7 @@ export var fullscreenExit =
14
14
  '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M5 16h3v3h2v-5H5v2zm3-8H5v2h5V5H8v3zm6 11h2v-3h3v-2h-5v5zm2-11V5h-2v5h5V8h-3z"/></svg>';
15
15
  export var format =
16
16
  '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M3 3v18h18v-2H5V3H3zm14 4V5l-4 4 4 4V9h4V7h-4zM7 9v2h10V9H7zm0 4v2h10v-2H7zm0 4v2h10v-2H7z"/></svg>';
17
+ export var snippet =
18
+ '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M14 2H6c-1.1 0-1.99.9-1.99 2L4 20c0 1.1.89 2 1.99 2H18c1.1 0 2-.9 2-2V8l-6-6zm2 16H8v-2h8v2zm0-4H8v-2h8v2zm-3-5V3.5L18.5 9H13z"/></svg>';
19
+ export var chevronDown =
20
+ '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M7.41 8.59L12 13.17l4.59-4.58L18 10l-6 6-6-6 1.41-1.41z"/></svg>';
package/src/index.ts CHANGED
@@ -11,7 +11,7 @@ import { drawSvgStringAsElement, addClass, removeClass } from "@matdata/yasgui-u
11
11
  import * as Sparql from "./sparql";
12
12
  import * as imgs from "./imgs";
13
13
  import * as Autocompleter from "./autocompleters";
14
- import { merge, escape } from "lodash-es";
14
+ import { merge, mergeWith, escape } from "lodash-es";
15
15
 
16
16
  import getDefaults from "./defaults";
17
17
  import CodeMirror from "./CodeMirror";
@@ -56,6 +56,8 @@ export class Yasqe extends CodeMirror {
56
56
  private fullscreenBtn: HTMLButtonElement | undefined;
57
57
  private isFullscreen: boolean = false;
58
58
  private resizeWrapper?: HTMLDivElement;
59
+ private snippetsBar?: HTMLDivElement;
60
+ private snippetsClickHandler?: (e: MouseEvent) => void;
59
61
  public rootEl: HTMLDivElement;
60
62
  public storage: YStorage;
61
63
  public config: Config;
@@ -66,7 +68,13 @@ export class Yasqe extends CodeMirror {
66
68
  this.rootEl = document.createElement("div");
67
69
  this.rootEl.className = "yasqe";
68
70
  parent.appendChild(this.rootEl);
69
- this.config = merge({}, Yasqe.defaults, conf);
71
+ // Use mergeWith to replace arrays instead of merging them by index
72
+ // This ensures that snippets: [] properly overrides default snippets
73
+ this.config = mergeWith({}, Yasqe.defaults, conf, (objValue: any, srcValue: any) => {
74
+ if (Array.isArray(srcValue)) {
75
+ return srcValue;
76
+ }
77
+ });
70
78
  //inherit codemirror props
71
79
  const cm = (CodeMirror as any)(this.rootEl, this.config);
72
80
  //Assign our functions to the cm object. This is needed, as some functions (like the ctrl-enter callback)
@@ -80,6 +88,7 @@ export class Yasqe extends CodeMirror {
80
88
  //Do some post processing
81
89
  this.storage = new YStorage(Yasqe.storageNamespace);
82
90
  this.drawButtons();
91
+ this.drawSnippetsBar();
83
92
  const storageId = this.getStorageId();
84
93
  // this.getWrapperElement
85
94
  if (storageId) {
@@ -417,6 +426,165 @@ export class Yasqe extends CodeMirror {
417
426
  public getIsFullscreen() {
418
427
  return this.isFullscreen;
419
428
  }
429
+
430
+ private insertSnippet(code: string) {
431
+ const doc = this.getDoc();
432
+ const cursor = doc.getCursor();
433
+ doc.replaceRange(code, cursor);
434
+ // Move cursor to end of inserted code
435
+ const lines = code.split("\n");
436
+ const lastLine = lines[lines.length - 1];
437
+ doc.setCursor({
438
+ line: cursor.line + lines.length - 1,
439
+ ch: lines.length === 1 ? cursor.ch + lastLine.length : lastLine.length,
440
+ });
441
+ this.focus();
442
+ }
443
+
444
+ private drawSnippetsBar() {
445
+ // Check if snippets bar should be shown
446
+ const shouldShow =
447
+ this.config.showSnippetsBar &&
448
+ (this.persistentConfig?.showSnippetsBar === undefined || this.persistentConfig.showSnippetsBar) &&
449
+ this.config.snippets.length > 0;
450
+
451
+ if (!shouldShow) {
452
+ // Remove existing bar if present
453
+ if (this.snippetsBar) {
454
+ this.snippetsBar.remove();
455
+ this.snippetsBar = undefined;
456
+ }
457
+ return;
458
+ }
459
+
460
+ // Create snippets bar if it doesn't exist
461
+ if (!this.snippetsBar) {
462
+ this.snippetsBar = document.createElement("div");
463
+ addClass(this.snippetsBar, "yasqe_snippetsBar");
464
+ // Insert before the CodeMirror wrapper element
465
+ const cmWrapper = this.getWrapperElement();
466
+ cmWrapper.parentElement?.insertBefore(this.snippetsBar, cmWrapper);
467
+ }
468
+
469
+ // Clear existing content
470
+ this.snippetsBar.innerHTML = "";
471
+
472
+ const snippets = this.config.snippets;
473
+
474
+ // If 10 or fewer snippets, show all as buttons
475
+ if (snippets.length <= 10) {
476
+ snippets.forEach((snippet) => {
477
+ const btn = document.createElement("button");
478
+ addClass(btn, "yasqe_snippetButton");
479
+ btn.textContent = snippet.label;
480
+ btn.title = snippet.code;
481
+ btn.setAttribute("aria-label", `Insert ${snippet.label} snippet`);
482
+ btn.onclick = () => this.insertSnippet(snippet.code);
483
+ this.snippetsBar!.appendChild(btn);
484
+ });
485
+ } else {
486
+ // Group snippets by group property
487
+ const grouped: { [group: string]: Snippet[] } = {};
488
+ const ungrouped: Snippet[] = [];
489
+
490
+ snippets.forEach((snippet) => {
491
+ if (snippet.group) {
492
+ if (!grouped[snippet.group]) grouped[snippet.group] = [];
493
+ grouped[snippet.group].push(snippet);
494
+ } else {
495
+ ungrouped.push(snippet);
496
+ }
497
+ });
498
+
499
+ // Create dropdown for each group
500
+ Object.keys(grouped).forEach((groupName) => {
501
+ const dropdown = document.createElement("div");
502
+ addClass(dropdown, "yasqe_snippetDropdown");
503
+
504
+ const dropdownBtn = document.createElement("button");
505
+ addClass(dropdownBtn, "yasqe_snippetDropdownButton");
506
+ dropdownBtn.textContent = groupName + " ";
507
+ const chevron = drawSvgStringAsElement(imgs.chevronDown);
508
+ addClass(chevron, "chevronIcon");
509
+ dropdownBtn.appendChild(chevron);
510
+ dropdownBtn.setAttribute("aria-label", `${groupName} snippets`);
511
+ dropdownBtn.setAttribute("aria-expanded", "false");
512
+
513
+ const dropdownContent = document.createElement("div");
514
+ addClass(dropdownContent, "yasqe_snippetDropdownContent");
515
+ dropdownContent.setAttribute("role", "menu");
516
+
517
+ grouped[groupName].forEach((snippet) => {
518
+ const item = document.createElement("button");
519
+ addClass(item, "yasqe_snippetDropdownItem");
520
+ item.textContent = snippet.label;
521
+ item.title = snippet.code;
522
+ item.setAttribute("role", "menuitem");
523
+ item.onclick = () => {
524
+ this.insertSnippet(snippet.code);
525
+ dropdownContent.style.display = "none";
526
+ dropdownBtn.setAttribute("aria-expanded", "false");
527
+ };
528
+ dropdownContent.appendChild(item);
529
+ });
530
+
531
+ dropdownBtn.onclick = (e) => {
532
+ e.stopPropagation();
533
+ const isOpen = dropdownContent.style.display === "block";
534
+ // Close all other dropdowns
535
+ const allDropdowns = this.snippetsBar!.querySelectorAll(".yasqe_snippetDropdownContent");
536
+ allDropdowns.forEach((dd) => {
537
+ (dd as HTMLElement).style.display = "none";
538
+ });
539
+ const allButtons = this.snippetsBar!.querySelectorAll(".yasqe_snippetDropdownButton");
540
+ allButtons.forEach((btn) => btn.setAttribute("aria-expanded", "false"));
541
+
542
+ // Toggle this dropdown
543
+ if (!isOpen) {
544
+ dropdownContent.style.display = "block";
545
+ dropdownBtn.setAttribute("aria-expanded", "true");
546
+ }
547
+ };
548
+
549
+ dropdown.appendChild(dropdownBtn);
550
+ dropdown.appendChild(dropdownContent);
551
+ this.snippetsBar!.appendChild(dropdown);
552
+ });
553
+
554
+ // Add ungrouped snippets as individual buttons
555
+ ungrouped.forEach((snippet) => {
556
+ const btn = document.createElement("button");
557
+ addClass(btn, "yasqe_snippetButton");
558
+ btn.textContent = snippet.label;
559
+ btn.title = snippet.code;
560
+ btn.setAttribute("aria-label", `Insert ${snippet.label} snippet`);
561
+ btn.onclick = () => this.insertSnippet(snippet.code);
562
+ this.snippetsBar!.appendChild(btn);
563
+ });
564
+ }
565
+
566
+ // Set up click handler for closing dropdowns when clicking outside
567
+ // Remove any existing handler first
568
+ if (this.snippetsClickHandler) {
569
+ document.removeEventListener("click", this.snippetsClickHandler);
570
+ }
571
+
572
+ // Create and store the handler
573
+ this.snippetsClickHandler = (e: MouseEvent) => {
574
+ if (this.snippetsBar && !this.snippetsBar.contains(e.target as Node)) {
575
+ const allDropdowns = this.snippetsBar.querySelectorAll(".yasqe_snippetDropdownContent");
576
+ allDropdowns.forEach((dd) => {
577
+ (dd as HTMLElement).style.display = "none";
578
+ });
579
+ const allButtons = this.snippetsBar.querySelectorAll(".yasqe_snippetDropdownButton");
580
+ allButtons.forEach((btn) => btn.setAttribute("aria-expanded", "false"));
581
+ }
582
+ };
583
+
584
+ // Add the handler
585
+ document.addEventListener("click", this.snippetsClickHandler);
586
+ }
587
+
420
588
  private drawResizer() {
421
589
  if (this.resizeWrapper) return;
422
590
  this.resizeWrapper = document.createElement("div");
@@ -954,6 +1122,31 @@ export class Yasqe extends CodeMirror {
954
1122
  }
955
1123
  }
956
1124
 
1125
+ /**
1126
+ * Snippets management
1127
+ */
1128
+ public setSnippetsBarVisible(visible: boolean) {
1129
+ if (!this.persistentConfig) {
1130
+ this.persistentConfig = {
1131
+ query: this.getValue(),
1132
+ editorHeight: this.config.editorHeight,
1133
+ showSnippetsBar: visible,
1134
+ };
1135
+ } else {
1136
+ this.persistentConfig.showSnippetsBar = visible;
1137
+ }
1138
+ this.saveQuery();
1139
+ this.drawSnippetsBar();
1140
+ }
1141
+
1142
+ public getSnippetsBarVisible(): boolean {
1143
+ return (
1144
+ this.config.showSnippetsBar &&
1145
+ (this.persistentConfig?.showSnippetsBar === undefined || this.persistentConfig.showSnippetsBar) &&
1146
+ this.config.snippets.length > 0
1147
+ );
1148
+ }
1149
+
957
1150
  /**
958
1151
  * Autocompleter management
959
1152
  */
@@ -1069,6 +1262,10 @@ export class Yasqe extends CodeMirror {
1069
1262
  this.unregisterEventListeners();
1070
1263
  this.resizeWrapper?.removeEventListener("mousedown", this.initDrag, false);
1071
1264
  this.resizeWrapper?.removeEventListener("dblclick", this.expandEditor);
1265
+ if (this.snippetsClickHandler) {
1266
+ document.removeEventListener("click", this.snippetsClickHandler);
1267
+ this.snippetsClickHandler = undefined;
1268
+ }
1072
1269
  for (const autocompleter in this.autocompleters) {
1073
1270
  this.disableCompleter(autocompleter);
1074
1271
  }
@@ -1164,6 +1361,10 @@ export interface HintConfig {
1164
1361
  ) => void;
1165
1362
  };
1166
1363
  }
1364
+ export interface BasicAuthConfig {
1365
+ username: string;
1366
+ password: string;
1367
+ }
1167
1368
  export interface RequestConfig<Y> {
1168
1369
  queryArgument: string | ((yasqe: Y) => string) | undefined;
1169
1370
  endpoint: string | ((yasqe: Y) => string);
@@ -1177,6 +1378,7 @@ export interface RequestConfig<Y> {
1177
1378
  headers: { [key: string]: string } | ((yasqe: Y) => { [key: string]: string });
1178
1379
  withCredentials: boolean | ((yasqe: Y) => boolean);
1179
1380
  adjustQueryBeforeRequest: ((yasqe: Y) => string) | false;
1381
+ basicAuth: BasicAuthConfig | ((yasqe: Y) => BasicAuthConfig | undefined) | undefined;
1180
1382
  }
1181
1383
  export type PlainRequestConfig = {
1182
1384
  [K in keyof RequestConfig<any>]: Exclude<RequestConfig<any>[K], Function>;
@@ -1184,6 +1386,12 @@ export type PlainRequestConfig = {
1184
1386
  export type PartialConfig = {
1185
1387
  [P in keyof Config]?: Config[P] extends object ? Partial<Config[P]> : Config[P];
1186
1388
  };
1389
+
1390
+ export interface Snippet {
1391
+ label: string;
1392
+ code: string;
1393
+ group?: string;
1394
+ }
1187
1395
  export interface Config extends Partial<CodeMirror.EditorConfiguration> {
1188
1396
  mode: string;
1189
1397
  collapsePrefixesOnLoad: boolean;
@@ -1223,12 +1431,15 @@ export interface Config extends Partial<CodeMirror.EditorConfiguration> {
1223
1431
  prefixCcApi: string; // the suggested default prefixes URL API getter
1224
1432
  showFormatButton: boolean; // Show a button to format the query
1225
1433
  checkConstructVariables: boolean; // Check for undefined variables in CONSTRUCT queries
1434
+ snippets: Snippet[]; // Code snippets to show in the snippets bar
1435
+ showSnippetsBar: boolean; // Show the snippets bar
1226
1436
  }
1227
1437
  export interface PersistentConfig {
1228
1438
  query: string;
1229
1439
  editorHeight: string;
1230
1440
  formatterType?: "sparql-formatter" | "legacy"; // Which formatter to use
1231
1441
  autoformatOnQuery?: boolean; // Auto-format query on execution
1442
+ showSnippetsBar?: boolean; // User preference for snippets bar visibility
1232
1443
  }
1233
1444
  // export var _Yasqe = _Yasqe;
1234
1445
 
@@ -292,4 +292,110 @@
292
292
  height: calc(100vh - 150px) !important;
293
293
  }
294
294
  }
295
+
296
+ .yasqe_snippetsBar {
297
+ position: relative;
298
+ display: flex;
299
+ flex-wrap: wrap;
300
+ gap: 5px;
301
+ padding: 5px 10px;
302
+ background-color: var(--yasgui-bg-primary, #fff);
303
+ border-bottom: 1px solid var(--yasgui-border-color, #e3e3e3);
304
+ order: -1; // Place before CodeMirror in flex layout
305
+
306
+ .yasqe_snippetButton {
307
+ padding: 4px 10px;
308
+ font-size: 12px;
309
+ border: 1px solid var(--yasgui-border-color, #ccc);
310
+ background-color: var(--yasgui-bg-primary, #fff);
311
+ border-radius: 3px;
312
+ cursor: pointer;
313
+ white-space: nowrap;
314
+ color: var(--yasgui-text-primary, #333);
315
+
316
+ &:hover {
317
+ background-color: var(--yasgui-bg-secondary, #f0f0f0);
318
+ border-color: var(--yasgui-text-muted, #999);
319
+ }
320
+
321
+ &:active {
322
+ background-color: var(--yasgui-bg-tertiary, #e0e0e0);
323
+ }
324
+ }
325
+
326
+ .yasqe_snippetDropdown {
327
+ position: relative;
328
+ display: inline-block;
329
+
330
+ .yasqe_snippetDropdownButton {
331
+ padding: 4px 10px;
332
+ font-size: 12px;
333
+ border: 1px solid var(--yasgui-border-color, #ccc);
334
+ background-color: var(--yasgui-bg-primary, #fff);
335
+ border-radius: 3px;
336
+ cursor: pointer;
337
+ white-space: nowrap;
338
+ color: var(--yasgui-text-primary, #333);
339
+ display: flex;
340
+ align-items: center;
341
+ gap: 4px;
342
+
343
+ .chevronIcon {
344
+ width: 16px;
345
+ height: 16px;
346
+ svg {
347
+ width: 16px;
348
+ height: 16px;
349
+ fill: var(--yasgui-text-secondary, #505050);
350
+ }
351
+ }
352
+
353
+ &:hover {
354
+ background-color: var(--yasgui-bg-secondary, #f0f0f0);
355
+ border-color: var(--yasgui-text-muted, #999);
356
+ }
357
+
358
+ &[aria-expanded="true"] {
359
+ background-color: var(--yasgui-bg-tertiary, #e0e0e0);
360
+ }
361
+ }
362
+
363
+ .yasqe_snippetDropdownContent {
364
+ display: none;
365
+ position: absolute;
366
+ top: 100%;
367
+ left: 0;
368
+ margin-top: 2px;
369
+ background-color: var(--yasgui-bg-primary, #fff);
370
+ border: 1px solid var(--yasgui-border-color, #ccc);
371
+ border-radius: 3px;
372
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
373
+ z-index: 100;
374
+ min-width: 150px;
375
+ max-height: 300px;
376
+ overflow-y: auto;
377
+
378
+ .yasqe_snippetDropdownItem {
379
+ display: block;
380
+ width: 100%;
381
+ padding: 6px 12px;
382
+ border: none;
383
+ background: none;
384
+ text-align: left;
385
+ cursor: pointer;
386
+ font-size: 12px;
387
+ color: var(--yasgui-text-primary, #333);
388
+ white-space: nowrap;
389
+
390
+ &:hover {
391
+ background-color: var(--yasgui-bg-secondary, #f0f0f0);
392
+ }
393
+
394
+ &:active {
395
+ background-color: var(--yasgui-bg-tertiary, #e0e0e0);
396
+ }
397
+ }
398
+ }
399
+ }
400
+ }
295
401
  }
package/src/sparql.ts CHANGED
@@ -16,6 +16,47 @@ function getRequestConfigSettings(yasqe: Yasqe, conf?: Partial<Config["requestCo
16
16
  }
17
17
  return (conf ?? {}) as RequestConfig<Yasqe>;
18
18
  }
19
+
20
+ /**
21
+ * Create a Basic Authentication header value
22
+ */
23
+ function createBasicAuthHeader(username: string, password: string): string {
24
+ const credentials = `${username}:${password}`;
25
+ const encoded = base64EncodeUnicode(credentials);
26
+ return `Basic ${encoded}`;
27
+ }
28
+
29
+ /**
30
+ * Base64-encode a Unicode string using UTF-8 encoding.
31
+ * This avoids errors with btoa() and supports all Unicode characters.
32
+ */
33
+ function base64EncodeUnicode(str: string): string {
34
+ if (typeof window !== "undefined" && typeof window.TextEncoder !== "undefined") {
35
+ const utf8Bytes = new window.TextEncoder().encode(str);
36
+ let binary = "";
37
+ for (let i = 0; i < utf8Bytes.length; i++) {
38
+ binary += String.fromCharCode(utf8Bytes[i]);
39
+ }
40
+ return btoa(binary);
41
+ } else if (typeof TextEncoder !== "undefined") {
42
+ // For environments where TextEncoder is global (e.g., Node.js)
43
+ const utf8Bytes = new TextEncoder().encode(str);
44
+ let binary = "";
45
+ for (let i = 0; i < utf8Bytes.length; i++) {
46
+ binary += String.fromCharCode(utf8Bytes[i]);
47
+ }
48
+ return btoa(binary);
49
+ } else {
50
+ // Fallback: try btoa directly, but warn about possible errors
51
+ try {
52
+ return btoa(str);
53
+ } catch (e) {
54
+ throw new Error(
55
+ "Basic authentication credentials contain unsupported Unicode characters. Please use a modern browser or restrict credentials to Latin1 characters.",
56
+ );
57
+ }
58
+ }
59
+ }
19
60
  // type callback = AjaxConfig.callbacks['complete'];
20
61
  export function getAjaxConfig(
21
62
  yasqe: Yasqe,
@@ -38,11 +79,30 @@ export function getAjaxConfig(
38
79
  const headers = isFunction(config.headers) ? config.headers(yasqe) : config.headers;
39
80
  // console.log({headers})
40
81
  const withCredentials = isFunction(config.withCredentials) ? config.withCredentials(yasqe) : config.withCredentials;
82
+
83
+ // Add Basic Authentication header if configured
84
+ const finalHeaders = { ...headers };
85
+ try {
86
+ const basicAuth = isFunction(config.basicAuth) ? config.basicAuth(yasqe) : config.basicAuth;
87
+ if (basicAuth && basicAuth.username && basicAuth.password) {
88
+ if (finalHeaders["Authorization"] !== undefined) {
89
+ console.warn(
90
+ "Authorization header already exists in request headers; skipping Basic Auth header to avoid overwrite.",
91
+ );
92
+ } else {
93
+ finalHeaders["Authorization"] = createBasicAuthHeader(basicAuth.username, basicAuth.password);
94
+ }
95
+ }
96
+ } catch (error) {
97
+ console.warn("Failed to configure basic authentication:", error);
98
+ // Continue without authentication if there's an error
99
+ }
100
+
41
101
  return {
42
102
  reqMethod,
43
103
  url: endpoint,
44
104
  args: getUrlArguments(yasqe, config),
45
- headers: headers,
105
+ headers: finalHeaders,
46
106
  accept: getAcceptHeader(yasqe, config),
47
107
  withCredentials,
48
108
  };