@teaflask/assistant 0.7.0 → 0.8.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.
@@ -55,10 +55,7 @@ function _renderNode(node, depth, lines) {
55
55
  function _renderSelectOptions(select, depth, lines) {
56
56
  const indent = _indentOf(depth);
57
57
  for (const option of Array.from(select.options)) {
58
- const text = option.text
59
- .replace(/\s+/g, " ")
60
- .trim()
61
- .substring(0, MAX_OPTION_TEXT_LENGTH);
58
+ const text = renderedOptionTextOf(option);
62
59
  let line = `${indent}- option "${_escapedText(text)}"`;
63
60
  if (option.selected) {
64
61
  line += " (selected)";
@@ -115,3 +112,9 @@ function _escapedText(text) {
115
112
  function _indentOf(depth) {
116
113
  return " ".repeat(depth);
117
114
  }
115
+ export function renderedOptionTextOf(option) {
116
+ return option.text
117
+ .replace(/\s+/g, " ")
118
+ .trim()
119
+ .substring(0, MAX_OPTION_TEXT_LENGTH);
120
+ }
@@ -1,3 +1,4 @@
1
+ import { parentAcrossShadowBoundaryOf } from "./visibility.js";
1
2
  const IMPLICIT_ROLE_BY_TAG = {
2
3
  a: "link",
3
4
  button: "button",
@@ -131,7 +132,7 @@ export const ARIA_CHECKED_ROLES = new Set([
131
132
  "menuitemradio",
132
133
  "switch",
133
134
  ]);
134
- export const ARIA_DISABLED_ROLES = new Set([
135
+ const ARIA_DISABLED_ROLES = new Set([
135
136
  "application",
136
137
  "button",
137
138
  "composite",
@@ -213,6 +214,32 @@ export function ariaBooleanOf(attribute) {
213
214
  }
214
215
  return attribute.toLowerCase() === "true";
215
216
  }
217
+ export function theControlIsDisabled(element, role) {
218
+ if (!ARIA_DISABLED_ROLES.has(role)) {
219
+ return false;
220
+ }
221
+ if (_isNativelyDisabled(element)) {
222
+ return true;
223
+ }
224
+ for (let current = element; current !== null; current = parentAcrossShadowBoundaryOf(current)) {
225
+ if (ariaBooleanOf(current.getAttribute("aria-disabled")) === true) {
226
+ return true;
227
+ }
228
+ }
229
+ return false;
230
+ }
231
+ function _isNativelyDisabled(element) {
232
+ const tag = element.tagName.toLowerCase();
233
+ if (!["button", "input", "select", "textarea", "option", "optgroup"].includes(tag)) {
234
+ return false;
235
+ }
236
+ try {
237
+ return element.matches(":disabled");
238
+ }
239
+ catch {
240
+ return false;
241
+ }
242
+ }
216
243
  function _implicitRoleOf(element) {
217
244
  const tag = element.tagName.toLowerCase();
218
245
  if (isContentEditableElement(element)) {
@@ -1,8 +1,7 @@
1
1
  import { accessibleNameOf } from "./accessible-name.js";
2
2
  import { CONTENT_REF_ROLES, INTERACTIVE_REF_ROLES, interactivitySignalsOf, isScrollable, } from "./interactivity.js";
3
3
  import { textboxValueOf } from "./redaction.js";
4
- import { ariaBooleanOf, ariaLevelOf, ARIA_CHECKED_ROLES, ARIA_DISABLED_ROLES, ARIA_SELECTED_ROLES, roleOf, } from "./roles.js";
5
- import { parentAcrossShadowBoundaryOf } from "./visibility.js";
4
+ import { ariaBooleanOf, ariaLevelOf, ARIA_CHECKED_ROLES, ARIA_SELECTED_ROLES, roleOf, theControlIsDisabled, } from "./roles.js";
6
5
  const MAX_NODE_NAME_LENGTH = 100;
7
6
  export function _snapshotNodeOf(element, context, refFor) {
8
7
  const { options, oracle, nameCache } = context;
@@ -35,7 +34,7 @@ export function _snapshotNodeOf(element, context, refFor) {
35
34
  checked: _checkedStateOf(element, roleLower),
36
35
  selected: _selectedStateOf(element, roleLower),
37
36
  focused: element.ownerDocument.activeElement === element,
38
- disabled: _disabledStateOf(element, roleLower),
37
+ disabled: theControlIsDisabled(element, roleLower),
39
38
  offscreen: !hidden && oracle.isOffscreen(element),
40
39
  refId,
41
40
  hints,
@@ -63,32 +62,6 @@ function _selectedStateOf(element, role) {
63
62
  }
64
63
  return ariaBooleanOf(element.getAttribute("aria-selected")) === true;
65
64
  }
66
- function _disabledStateOf(element, role) {
67
- if (!ARIA_DISABLED_ROLES.has(role)) {
68
- return false;
69
- }
70
- if (_isNativelyDisabled(element)) {
71
- return true;
72
- }
73
- for (let current = element; current !== null; current = parentAcrossShadowBoundaryOf(current)) {
74
- if (ariaBooleanOf(current.getAttribute("aria-disabled")) === true) {
75
- return true;
76
- }
77
- }
78
- return false;
79
- }
80
- function _isNativelyDisabled(element) {
81
- const tag = element.tagName.toLowerCase();
82
- if (!["button", "input", "select", "textarea", "option", "optgroup"].includes(tag)) {
83
- return false;
84
- }
85
- try {
86
- return element.matches(":disabled");
87
- }
88
- catch {
89
- return false;
90
- }
91
- }
92
65
  export function _normalizedNodeName(name) {
93
66
  return name.replace(/\s+/g, " ").trim().substring(0, MAX_NODE_NAME_LENGTH);
94
67
  }
@@ -34,17 +34,23 @@ export function _resolvedRootOf(options, elementByRef) {
34
34
  return { root: body, error: null };
35
35
  }
36
36
  function _rootUnlessCurated(root, options) {
37
- for (let current = root; current !== null; current = parentAcrossShadowBoundaryOf(current)) {
38
- if (_theSubtreeIsCurated(current, options.ignoreSelectors)) {
39
- return {
40
- root: null,
41
- error: "That part of the page has opted out of page reads — " +
42
- "take a full read to see what is available.",
43
- };
44
- }
37
+ if (theElementSitsInACuratedSubtree(root, options.ignoreSelectors)) {
38
+ return {
39
+ root: null,
40
+ error: "That part of the page has opted out of page reads — " +
41
+ "take a full read to see what is available.",
42
+ };
45
43
  }
46
44
  return { root, error: null };
47
45
  }
46
+ export function theElementSitsInACuratedSubtree(element, ignoreSelectors) {
47
+ for (let current = element; current !== null; current = parentAcrossShadowBoundaryOf(current)) {
48
+ if (_theSubtreeIsCurated(current, ignoreSelectors)) {
49
+ return true;
50
+ }
51
+ }
52
+ return false;
53
+ }
48
54
  function _safeQuerySelector(selector) {
49
55
  try {
50
56
  const direct = document.querySelector(selector);
@@ -18,6 +18,10 @@ const SKIP_TAGS = new Set([
18
18
  let _refCounter = 0;
19
19
  const _refBadgeByElement = new WeakMap();
20
20
  const _elementByRef = new Map();
21
+ let _lastReadIgnoreSelectors = [];
22
+ export function lastReadIgnoreSelectors() {
23
+ return _lastReadIgnoreSelectors;
24
+ }
21
25
  export function snapshotTreeOf(options) {
22
26
  const context = {
23
27
  options,
@@ -30,6 +34,7 @@ export function snapshotTreeOf(options) {
30
34
  }
31
35
  const root = rooting.root;
32
36
  _elementByRef.clear();
37
+ _lastReadIgnoreSelectors = options.ignoreSelectors;
33
38
  const fragment = _fragmentNode();
34
39
  _traverse(root, 0, fragment, context);
35
40
  _mergeStringChildren(fragment);
@@ -52,7 +57,7 @@ export function resolvedElementOfRef(refId) {
52
57
  rootElement: null,
53
58
  refId,
54
59
  selector: null,
55
- ignoreSelectors: [],
60
+ ignoreSelectors: _lastReadIgnoreSelectors,
56
61
  };
57
62
  const rooting = _resolvedRootOf(address, _elementByRef);
58
63
  if (rooting.error !== null) {
@@ -27,19 +27,96 @@ export function actionIntentOf(wire) {
27
27
  if (wire.body !== null && wire.body !== undefined) {
28
28
  intent.body = wire.body;
29
29
  }
30
+ const graphql = _graphqlOf(wire);
31
+ if (graphql !== null) {
32
+ intent.graphql = graphql;
33
+ }
30
34
  return intent;
31
35
  }
36
+ function _graphqlOf(wire) {
37
+ const descriptor = _recordOf(wire.graphql);
38
+ const body = _recordOf(wire.body);
39
+ if (descriptor === null || body === null) {
40
+ return null;
41
+ }
42
+ const halves = {
43
+ operationName: _nonEmptyStringOf(descriptor.operation_name),
44
+ operationType: descriptor.operation_type,
45
+ document: _nonEmptyStringOf(body.query),
46
+ variables: body.variables === undefined || body.variables === null
47
+ ? {}
48
+ : _recordOf(body.variables),
49
+ };
50
+ if (!_descriptorAgreesWithBody(halves, body.operationName)) {
51
+ return null;
52
+ }
53
+ const { operationName, operationType, document, variables } = halves;
54
+ return { operationName, operationType, document, variables };
55
+ }
56
+ function _descriptorAgreesWithBody(halves, bodyOperationName) {
57
+ return (halves.operationName !== null &&
58
+ halves.operationName === bodyOperationName &&
59
+ (halves.operationType === "query" || halves.operationType === "mutation") &&
60
+ halves.document !== null &&
61
+ halves.variables !== null);
62
+ }
32
63
  export async function performActionIntent(adapter, intent) {
64
+ const land = intent.graphql === undefined ? _landableBodyOf : _landableGraphQLBodyOf;
33
65
  if (adapter.kind === "request") {
34
- return _intentResultOf(200, await adapter.execute(intent));
66
+ return _intentResultOf(200, land(await adapter.execute(intent)));
35
67
  }
36
68
  const response = await fetch(_urlOf(adapter.baseUrl, intent), await _requestInitOf(adapter, intent));
37
- return _resultOf(response);
69
+ return _resultOf(response, land);
38
70
  }
39
- function _intentResultOf(status, rawBody) {
40
- const landed = _landableBodyOf(rawBody);
71
+ function _intentResultOf(status, landed) {
41
72
  return { status, body: landed.body, truncated: landed.truncated };
42
73
  }
74
+ function _landableGraphQLBodyOf(body) {
75
+ const whole = _landableBodyOf(body);
76
+ const errored = whole.truncated ? _erroredResponseOf(body) : null;
77
+ if (errored === null) {
78
+ return whole;
79
+ }
80
+ return (_errorsAndDataWhole(errored) ??
81
+ _errorsWithDataCut(errored) ??
82
+ _wholeCutErrorsFirst(errored));
83
+ }
84
+ function _erroredResponseOf(body) {
85
+ const response = _recordOf(body);
86
+ if (response === null) {
87
+ return null;
88
+ }
89
+ const errors = JSON.parse(JSON.stringify(response.errors ?? null));
90
+ if (!Array.isArray(errors) || errors.length === 0) {
91
+ return null;
92
+ }
93
+ return { errors, data: response.data ?? null, response };
94
+ }
95
+ function _errorsAndDataWhole({ errors, data, }) {
96
+ const kept = { errors, data };
97
+ return doorLengthCeilingOf(JSON.stringify(kept)) <= RESPONSE_BODY_MAX_CHARS
98
+ ? { body: kept, truncated: true }
99
+ : null;
100
+ }
101
+ function _errorsWithDataCut({ errors, data, }) {
102
+ const dataBudget = RESPONSE_BODY_MAX_CHARS -
103
+ doorLengthCeilingOf(JSON.stringify({ errors, data: "" })) -
104
+ doorCostOfText(TRUNCATION_NOTICE);
105
+ if (dataBudget < 0) {
106
+ return null;
107
+ }
108
+ const dataText = JSON.stringify(data);
109
+ const bounded = boundedTextOf(dataText, Math.min(dataBudget, doorCostOfText(dataText) - 1));
110
+ return {
111
+ body: { errors, data: bounded + TRUNCATION_NOTICE },
112
+ truncated: true,
113
+ };
114
+ }
115
+ function _wholeCutErrorsFirst({ errors, response, }) {
116
+ const rest = { ...response };
117
+ delete rest.errors;
118
+ return _landableBodyOf(JSON.stringify({ errors, ...rest }));
119
+ }
43
120
  function _landableBodyOf(body) {
44
121
  if (body === undefined) {
45
122
  return { body: null, truncated: false };
@@ -105,8 +182,8 @@ function _queryStringOf(query) {
105
182
  const encoded = parameters.toString();
106
183
  return encoded === "" ? "" : `?${encoded}`;
107
184
  }
108
- async function _resultOf(response) {
109
- return _intentResultOf(response.status, _jsonOrTextOf(await response.text()));
185
+ async function _resultOf(response, land) {
186
+ return _intentResultOf(response.status, land(_jsonOrTextOf(await response.text())));
110
187
  }
111
188
  function _jsonOrTextOf(text) {
112
189
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@teaflask/assistant",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "description": "The AI assistant for your product: drop it into your React app or any page with a script tag.",
5
5
  "license": "FSL-1.1-ALv2",
6
6
  "author": "Teaflask",