@nodii/dev 0.6.17 → 0.6.21
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/README.md +50 -3
- package/dist/cli.js +73 -24
- package/dist/core.js +23 -8
- package/dist/design.js +15 -9
- package/dist/doctor.js +6 -3
- package/dist/figma.js +168 -8
- package/dist/lanes.js +23 -1
- package/dist/oauth.js +149 -11
- package/dist/vendor/ds-conformance.mjs +259 -21
- package/package.json +1 -1
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// ds-conformance.mjs: the design-system conformance check (DEC-0034 MW6-14 to
|
|
2
|
-
// MW6-17, as amended by DEC-0035, DEC-0036 and DEC-0045
|
|
2
|
+
// MW6-17, as amended by DEC-0035, DEC-0036 and DEC-0045, and scoped by
|
|
3
|
+
// Dev issue 116).
|
|
3
4
|
//
|
|
4
5
|
// Canonical source: design-system `infra/figma/ds-conformance.mjs`. Every copy
|
|
5
6
|
// (Dev's submit gate, nodii-clis' `design check`) vendors these
|
|
@@ -20,13 +21,16 @@
|
|
|
20
21
|
// remote component, from the same file for a local one), or
|
|
21
22
|
// null when it can't be resolved
|
|
22
23
|
// options: { library?: boolean, declaredRoles?: string[] }
|
|
23
|
-
// returns { version, findings, staleOverrides, libraryUpdatePending
|
|
24
|
-
// each a list of { nodeId, check,
|
|
24
|
+
// returns { version, findings, staleOverrides, libraryUpdatePending,
|
|
25
|
+
// outOfScope, outOfScopeNodes }, each list a list of { nodeId, check,
|
|
26
|
+
// detail }; only `findings` fail. `outOfScope` lists the roots of what
|
|
27
|
+
// the app-UI gate doesn't check (the Email kit), `outOfScopeNodes` how
|
|
28
|
+
// many nodes they hold
|
|
25
29
|
// neededMains(nodes, mains, options) -> componentIds still to fetch; call
|
|
26
30
|
// it until it returns [] (each round can reveal instances inside slot
|
|
27
31
|
// content).
|
|
28
32
|
|
|
29
|
-
export const VERSION = "1.0.
|
|
33
|
+
export const VERSION = "1.0.4";
|
|
30
34
|
|
|
31
35
|
/** @typedef {{ nodeId: string, check: string, detail: string }} Finding */
|
|
32
36
|
/**
|
|
@@ -118,13 +122,30 @@ const TEXT_STYLE_FIELDS = new Set([
|
|
|
118
122
|
const STYLE_FIELD = /^(inherit)?(Fill|Stroke|Text|Effect|Grid)StyleId$/i;
|
|
119
123
|
|
|
120
124
|
const SURFACE_NAME = /^Surface \//;
|
|
125
|
+
// Email is out of the app-UI gate's scope (Dev issue 116): the Email kit's
|
|
126
|
+
// twelve registered parts (DESIGN-SYSTEM-33), an email (a frame named Email
|
|
127
|
+
// that places only those parts), and the template editor's block palette.
|
|
128
|
+
const EMAIL_KIT =
|
|
129
|
+
/^Email ?\/ ?(Header|Alert strip|Heading|Amount block|Button|Link fallback|Divider|Key-value row|Security note|Code box|QR pay block|Footer)$/i;
|
|
130
|
+
const EMAIL_FRAME = /^email\b/i;
|
|
131
|
+
const BLOCK_PALETTE = /^(email )?blocks? (panel|palette)$/i;
|
|
132
|
+
// A column of a table, grid or list; a page named "List Page" is no column.
|
|
133
|
+
const TABLE_CONTAINER = /table|list|grid/i;
|
|
134
|
+
const NOT_A_TABLE = /\blist page\b/i;
|
|
121
135
|
const CUSTOMER_HEADER =
|
|
122
136
|
/^(customer|client|contact|lead|applicant|candidate)s?( name)?$/i;
|
|
123
137
|
const EXTERNAL_PLANE = /customer portal|partner room|external/i;
|
|
124
138
|
// The viewer's own record (R5): the App Frame sidebar footer, the user menu
|
|
125
139
|
// and the profile. Not any footer: a dialog or table footer can list others.
|
|
126
140
|
const SELF_CONTAINER =
|
|
127
|
-
/sidebar ?footer|user button|user menu|account menu|profile/i;
|
|
141
|
+
/sidebar ?footer|user button|user menu|account menu|profile|\b(my|own) (account|tokens?|security|sessions?|settings|notification settings|records?)\b/i;
|
|
142
|
+
// A frame the screen marks as the viewer's own: "[self]", "(self)", a
|
|
143
|
+
// trailing "· self", or "(you)".
|
|
144
|
+
const SELF_MARK = /\[self\]|\(self\)|·\s*self\s*$|\(you\)/i;
|
|
145
|
+
// Where the screens show who the viewer is: the App Frame footer, the user
|
|
146
|
+
// and account menus, and frames marked self or named my/own.
|
|
147
|
+
const VIEWER_PLACE =
|
|
148
|
+
/sidebar ?footer|user button|user menu|account menu|\b(my|own) (profile|account|tokens?|security|sessions?|settings)\b/i;
|
|
128
149
|
const SELF_TEXT = /\(you\)|^you$/i;
|
|
129
150
|
const SIGNED_IN = /^(signed in( as\b.*)?|not you\?.*)$/i;
|
|
130
151
|
// The signed-in account card (Account Context, kind=Account) shows the
|
|
@@ -143,6 +164,16 @@ const PHONE = /(?<![\w.+\-/])\+?\(?\d[\d ().-]{7,}\d(?![\w\-/])/g;
|
|
|
143
164
|
const LOCAL_PHONE = /^(\(\d{3,5}\)[ .-]?|\d{3,5}[ .-])\d{3,5}([ .-]\d{2,5})?$/;
|
|
144
165
|
const DATE = /^(\d{4}-\d{2}-\d{2}|\d{1,2}[-/.]\d{1,2}[-/.]\d{2,4})/;
|
|
145
166
|
const ROLE_ANNOTATION = /\[role:([a-z0-9._-]+)\]/gi;
|
|
167
|
+
// otherPersonRevealed is about people (Dev issue 116): a value is judged
|
|
168
|
+
// by the field it fills. A tenant's business contact (support phone or
|
|
169
|
+
// email, caller or sender id, business address) is not personal.
|
|
170
|
+
const BUSINESS_FIELD =
|
|
171
|
+
/\b(support|help ?desk|helpline|hotline|toll[- ]free|caller id|sender( id)?|sending (address|domain|number)|from (address|email|number)|reply[- ]to|business|tenant)\b/i;
|
|
172
|
+
// A person's field wins: "Support agent" is a person.
|
|
173
|
+
const PERSON_FIELD =
|
|
174
|
+
/\b(user|member|customer|recipient|applicant|employee|candidate|assignee|owner|invitee|requester|reviewer|agent|engineer|staff|rep|manager|person|personal|your|my)\b/i;
|
|
175
|
+
// A field's frame names its label: field/Caller ID, select/Sender ID.
|
|
176
|
+
const FIELD_FRAME = /^(field|select|input|textarea|kv|detail|row) ?\/ ?(.+)$/i;
|
|
146
177
|
|
|
147
178
|
/** "I12:34;56:78" -> ["12:34", "56:78"]; "12:34" -> ["12:34"]. */
|
|
148
179
|
const segments = (id) => (id.startsWith("I") ? id.slice(1) : id).split(";");
|
|
@@ -325,6 +356,25 @@ function analyse(entry, mains, library = false) {
|
|
|
325
356
|
};
|
|
326
357
|
const componentLabel = (inst) =>
|
|
327
358
|
setOf(inst)?.name ?? componentOf(inst)?.name ?? inst.name;
|
|
359
|
+
// What the app-UI gate leaves out: each root, outermost first (byId is in
|
|
360
|
+
// document order, so a parent is decided before its children).
|
|
361
|
+
/** @type {Map<string, string>} */
|
|
362
|
+
const scopeRoots = new Map();
|
|
363
|
+
/** @type {Map<string, "kit" | "other" | "none">} */
|
|
364
|
+
const placedMemo = new Map();
|
|
365
|
+
const out = new Set();
|
|
366
|
+
for (const n of byId.values()) {
|
|
367
|
+
const p = parent.get(n.id);
|
|
368
|
+
if (p && out.has(p.id)) {
|
|
369
|
+
out.add(n.id);
|
|
370
|
+
continue;
|
|
371
|
+
}
|
|
372
|
+
const why = outOfScopeRoot(n, parent, componentLabel, placedMemo);
|
|
373
|
+
if (why) {
|
|
374
|
+
out.add(n.id);
|
|
375
|
+
scopeRoots.set(n.id, why);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
328
378
|
return {
|
|
329
379
|
root,
|
|
330
380
|
components,
|
|
@@ -340,7 +390,54 @@ function analyse(entry, mains, library = false) {
|
|
|
340
390
|
componentOf,
|
|
341
391
|
setOf,
|
|
342
392
|
componentLabel,
|
|
393
|
+
out,
|
|
394
|
+
scopeRoots,
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
const emailKitPart = (n, parent, componentLabel) =>
|
|
399
|
+
(n.type === "INSTANCE" && EMAIL_KIT.test(componentLabel(n) ?? "")) ||
|
|
400
|
+
((n.type === "COMPONENT" || n.type === "COMPONENT_SET") &&
|
|
401
|
+
(EMAIL_KIT.test(n.name ?? "") ||
|
|
402
|
+
(n.type === "COMPONENT" &&
|
|
403
|
+
EMAIL_KIT.test(parent.get(n.id)?.name ?? ""))));
|
|
404
|
+
/**
|
|
405
|
+
* Why a node and everything in it is outside the app-UI gate, or null: an
|
|
406
|
+
* Email kit part, an email made of them (a frame named Email whose only
|
|
407
|
+
* placed parts, at any depth, are the kit's), or the template editor's
|
|
408
|
+
* block palette.
|
|
409
|
+
*/
|
|
410
|
+
function outOfScopeRoot(n, parent, componentLabel, memo) {
|
|
411
|
+
if (emailKitPart(n, parent, componentLabel))
|
|
412
|
+
return `Email kit part ${n.type === "INSTANCE" ? componentLabel(n) : n.name}`;
|
|
413
|
+
if (n.type !== "FRAME" && n.type !== "GROUP" && n.type !== "SECTION")
|
|
414
|
+
return null;
|
|
415
|
+
if (BLOCK_PALETTE.test(n.name ?? "")) return `email block palette ${n.name}`;
|
|
416
|
+
// Every part placed in it, at any depth, is the kit's.
|
|
417
|
+
/** @returns {"kit" | "other" | "none"} */
|
|
418
|
+
const placed = (x) => {
|
|
419
|
+
const hit = memo.get(x.id);
|
|
420
|
+
if (hit) return hit;
|
|
421
|
+
let kind = /** @type {"kit" | "other" | "none"} */ ("none");
|
|
422
|
+
if (emailKitPart(x, parent, componentLabel)) kind = "kit";
|
|
423
|
+
else if (x.type === "INSTANCE") kind = "other";
|
|
424
|
+
else
|
|
425
|
+
for (const c of x.children ?? []) {
|
|
426
|
+
const k = placed(c);
|
|
427
|
+
if (k === "other") {
|
|
428
|
+
kind = "other";
|
|
429
|
+
break;
|
|
430
|
+
}
|
|
431
|
+
if (k === "kit") kind = "kit";
|
|
432
|
+
}
|
|
433
|
+
memo.set(x.id, kind);
|
|
434
|
+
return kind;
|
|
343
435
|
};
|
|
436
|
+
// An email is named so; an editor's workspace around it, or an app screen
|
|
437
|
+
// holding a kit part, stays in scope.
|
|
438
|
+
return EMAIL_FRAME.test(n.name ?? "") && placed(n) === "kit"
|
|
439
|
+
? `email ${n.name}`
|
|
440
|
+
: null;
|
|
344
441
|
}
|
|
345
442
|
|
|
346
443
|
/** Component ids whose mains the check still needs. */
|
|
@@ -350,7 +447,7 @@ export function neededMains(nodes, mains = {}, options = {}) {
|
|
|
350
447
|
if (!entry?.document) continue;
|
|
351
448
|
const a = analyse(entry, mains, Boolean(options.library));
|
|
352
449
|
for (const n of a.byId.values()) {
|
|
353
|
-
if (n.type !== "INSTANCE" || !n.componentId) continue;
|
|
450
|
+
if (n.type !== "INSTANCE" || !n.componentId || a.out.has(n.id)) continue;
|
|
354
451
|
if (Object.prototype.hasOwnProperty.call(mains, n.componentId)) continue;
|
|
355
452
|
// The screen's own instances (for overrides and slot content), and
|
|
356
453
|
// every table header cell (for its fill).
|
|
@@ -423,7 +520,8 @@ function textOf(n) {
|
|
|
423
520
|
* mains?: Record<string, NodesEntry | null>,
|
|
424
521
|
* options?: { library?: boolean, declaredRoles?: string[] } }} input
|
|
425
522
|
* @returns {{ version: string, findings: Finding[], staleOverrides: Finding[],
|
|
426
|
-
* libraryUpdatePending: Finding[]
|
|
523
|
+
* libraryUpdatePending: Finding[], outOfScope: Finding[],
|
|
524
|
+
* outOfScopeNodes: number }}
|
|
427
525
|
*/
|
|
428
526
|
export function checkConformance({
|
|
429
527
|
nodes,
|
|
@@ -440,6 +538,9 @@ export function checkConformance({
|
|
|
440
538
|
const staleOverrides = [];
|
|
441
539
|
/** @type {Finding[]} */
|
|
442
540
|
const libraryUpdatePending = [];
|
|
541
|
+
/** @type {Finding[]} */
|
|
542
|
+
const outOfScope = [];
|
|
543
|
+
const outNodes = new Set();
|
|
443
544
|
const seen = new Set();
|
|
444
545
|
const push = (list, nodeId, check, detail) => {
|
|
445
546
|
const key = `${nodeId}|${check}|${detail}`;
|
|
@@ -454,12 +555,46 @@ export function checkConformance({
|
|
|
454
555
|
labelRoles.set(label.trim().toLowerCase(), r);
|
|
455
556
|
const librarySetNames = new Set();
|
|
456
557
|
const usedRoles = new Set();
|
|
558
|
+
const analyses = Object.values(nodes ?? {})
|
|
559
|
+
.filter((entry) => entry?.document)
|
|
560
|
+
.map((entry) => analyse(entry, mains, library));
|
|
561
|
+
// The viewer (R5): the email and phone the screens show in the viewer's
|
|
562
|
+
// own places (the App Frame footer, a frame marked self). The same value
|
|
563
|
+
// anywhere else is still the viewer's own.
|
|
564
|
+
const viewer = new Set();
|
|
565
|
+
for (const a of analyses)
|
|
566
|
+
for (const n of a.byId.values()) {
|
|
567
|
+
if (n.type !== "TEXT" || a.out.has(n.id)) continue;
|
|
568
|
+
const ids = identifiers(textOf(n));
|
|
569
|
+
if (ids.length === 0) continue;
|
|
570
|
+
if (
|
|
571
|
+
a
|
|
572
|
+
.ancestors(n)
|
|
573
|
+
.some(
|
|
574
|
+
(x) =>
|
|
575
|
+
VIEWER_PLACE.test(x.name ?? "") || SELF_MARK.test(x.name ?? ""),
|
|
576
|
+
)
|
|
577
|
+
)
|
|
578
|
+
for (const id of ids) viewer.add(id);
|
|
579
|
+
}
|
|
457
580
|
|
|
458
|
-
for (const
|
|
459
|
-
if (!entry?.document) continue;
|
|
460
|
-
const a = analyse(entry, mains, library);
|
|
581
|
+
for (const a of analyses) {
|
|
461
582
|
for (const set of Object.values(a.componentSets))
|
|
462
583
|
if (set?.remote) librarySetNames.add(set.name);
|
|
584
|
+
for (const [nodeId, why] of a.scopeRoots) {
|
|
585
|
+
let count = 0;
|
|
586
|
+
for (const n of indexTree(a.byId.get(nodeId)).byId.values()) {
|
|
587
|
+
if (!outNodes.has(n.id)) count++;
|
|
588
|
+
outNodes.add(n.id);
|
|
589
|
+
}
|
|
590
|
+
if (count > 0)
|
|
591
|
+
push(
|
|
592
|
+
outOfScope,
|
|
593
|
+
nodeId,
|
|
594
|
+
"outOfScope",
|
|
595
|
+
`${why}: ${count} node${count === 1 ? "" : "s"} outside the app-UI gate (email, Dev issue 116)`,
|
|
596
|
+
);
|
|
597
|
+
}
|
|
463
598
|
// Every (node, field) the screen overrode on an instance it placed.
|
|
464
599
|
/** @type {Map<string, { fields: Set<string>, from: any }>} */
|
|
465
600
|
const overrideFields = new Map();
|
|
@@ -481,12 +616,16 @@ export function checkConformance({
|
|
|
481
616
|
// not; one equal to the main's value is a stale mark (DEC-0035).
|
|
482
617
|
for (const [id, { fields, from }] of overrideFields) {
|
|
483
618
|
const node = a.byId.get(id);
|
|
484
|
-
if (!node || !a.shown(node)) continue;
|
|
619
|
+
if (!node || !a.shown(node) || a.out.has(id)) continue;
|
|
485
620
|
const base = baselineInstance(a, mains, from, node);
|
|
486
621
|
if (!base) continue;
|
|
487
622
|
const mainNode = a.mainNodeFor(base, id);
|
|
488
623
|
if (!mainNode) continue;
|
|
489
624
|
const mainStyles = mains[base.componentId]?.styles ?? {};
|
|
625
|
+
// Slot content a library part ships by default: its value there is
|
|
626
|
+
// the library's, not this file's (DEC-0047).
|
|
627
|
+
/** @type {ReturnType<typeof defaultSlotContent> | undefined} */
|
|
628
|
+
let slotDefault;
|
|
490
629
|
const differ = [];
|
|
491
630
|
const equal = [];
|
|
492
631
|
for (const field of [...fields].sort()) {
|
|
@@ -494,7 +633,20 @@ export function checkConformance({
|
|
|
494
633
|
if (exemptPaint(a, from, node, field)) continue;
|
|
495
634
|
const mine = fieldValue(node, field, a.styles);
|
|
496
635
|
const theirs = fieldValue(mainNode, field, mainStyles);
|
|
497
|
-
if (sameValue(mine, theirs))
|
|
636
|
+
if (sameValue(mine, theirs)) {
|
|
637
|
+
equal.push(field);
|
|
638
|
+
continue;
|
|
639
|
+
}
|
|
640
|
+
if (slotDefault === undefined)
|
|
641
|
+
slotDefault = defaultSlotContent(a, mains, node);
|
|
642
|
+
if (
|
|
643
|
+
slotDefault &&
|
|
644
|
+
sameValue(
|
|
645
|
+
mine,
|
|
646
|
+
fieldValue(slotDefault.node, field, slotDefault.styles),
|
|
647
|
+
)
|
|
648
|
+
)
|
|
649
|
+
equal.push(field);
|
|
498
650
|
else
|
|
499
651
|
differ.push(
|
|
500
652
|
`${field} ${shortValue(mine)} (main ${shortValue(theirs)})`,
|
|
@@ -518,7 +670,7 @@ export function checkConformance({
|
|
|
518
670
|
}
|
|
519
671
|
|
|
520
672
|
for (const n of a.byId.values()) {
|
|
521
|
-
if (!a.shown(n)) continue;
|
|
673
|
+
if (!a.shown(n) || a.out.has(n.id)) continue;
|
|
522
674
|
const own = !a.composed(n);
|
|
523
675
|
|
|
524
676
|
if (n.type === "INSTANCE")
|
|
@@ -670,14 +822,17 @@ export function checkConformance({
|
|
|
670
822
|
revealedByPermission(a, n)
|
|
671
823
|
)
|
|
672
824
|
continue;
|
|
673
|
-
|
|
825
|
+
const business = businessField(a, n);
|
|
826
|
+
for (const m of business ? [] : text.matchAll(EMAIL)) {
|
|
674
827
|
const local = m[0].split("@")[0] ?? "";
|
|
675
828
|
if (MASKED.test(local) || ROLE_MAILBOX.test(local)) continue;
|
|
829
|
+
if (viewer.has(m[0].toLowerCase())) continue;
|
|
676
830
|
push(findings, n.id, "otherPersonRevealed", `unmasked email ${m[0]}`);
|
|
677
831
|
}
|
|
678
|
-
for (const m of text.matchAll(PHONE)) {
|
|
832
|
+
for (const m of business ? [] : text.matchAll(PHONE)) {
|
|
679
833
|
const digits = m[0].replace(/\D/g, "");
|
|
680
|
-
if (DATE.test(m[0]) || MASKED.test(m[0]))
|
|
834
|
+
if (DATE.test(m[0]) || MASKED.test(m[0]) || viewer.has(digits))
|
|
835
|
+
continue;
|
|
681
836
|
const phoneShaped =
|
|
682
837
|
m[0].startsWith("+") || LOCAL_PHONE.test(m[0].trim());
|
|
683
838
|
if (phoneShaped && digits.length >= 10 && digits.length <= 15)
|
|
@@ -703,14 +858,19 @@ export function checkConformance({
|
|
|
703
858
|
if (
|
|
704
859
|
header.type !== "TEXT" ||
|
|
705
860
|
!CUSTOMER_HEADER.test(textOf(header)) ||
|
|
706
|
-
!a.shown(header)
|
|
861
|
+
!a.shown(header) ||
|
|
862
|
+
a.out.has(header.id)
|
|
707
863
|
)
|
|
708
864
|
continue;
|
|
709
865
|
// A column header sits in a table, list or grid; a "Customer" form
|
|
710
866
|
// label does not head anything.
|
|
711
867
|
const table = a
|
|
712
868
|
.ancestors(header)
|
|
713
|
-
.find(
|
|
869
|
+
.find(
|
|
870
|
+
(x) =>
|
|
871
|
+
TABLE_CONTAINER.test(x.name ?? "") &&
|
|
872
|
+
!NOT_A_TABLE.test(x.name ?? ""),
|
|
873
|
+
);
|
|
714
874
|
// The header's cell bounds its column, unless the parent is the whole
|
|
715
875
|
// header row: then the header text's own box does.
|
|
716
876
|
const cell = a.parent.get(header.id);
|
|
@@ -723,7 +883,13 @@ export function checkConformance({
|
|
|
723
883
|
: header.absoluteBoundingBox;
|
|
724
884
|
if (!table || !box) continue;
|
|
725
885
|
for (const n of indexTree(table).byId.values()) {
|
|
726
|
-
if (
|
|
886
|
+
if (
|
|
887
|
+
n.type !== "TEXT" ||
|
|
888
|
+
n.id === header.id ||
|
|
889
|
+
!a.shown(n) ||
|
|
890
|
+
a.out.has(n.id)
|
|
891
|
+
)
|
|
892
|
+
continue;
|
|
727
893
|
if (!screenText(a, n, overridden)) continue;
|
|
728
894
|
const b = n.absoluteBoundingBox;
|
|
729
895
|
const text = textOf(n);
|
|
@@ -760,7 +926,74 @@ export function checkConformance({
|
|
|
760
926
|
`declared role ${role.role} has no ${role.componentName} instance`,
|
|
761
927
|
);
|
|
762
928
|
}
|
|
763
|
-
return {
|
|
929
|
+
return {
|
|
930
|
+
version: VERSION,
|
|
931
|
+
findings,
|
|
932
|
+
staleOverrides,
|
|
933
|
+
libraryUpdatePending,
|
|
934
|
+
outOfScope,
|
|
935
|
+
outOfScopeNodes: outNodes.size,
|
|
936
|
+
};
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
/** The emails (lower-cased) and phone digits a text shows. */
|
|
940
|
+
function identifiers(text) {
|
|
941
|
+
return [
|
|
942
|
+
...[...text.matchAll(EMAIL)].map((m) => m[0].toLowerCase()),
|
|
943
|
+
...[...text.matchAll(PHONE)].map((m) => m[0].replace(/\D/g, "")),
|
|
944
|
+
];
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
/**
|
|
948
|
+
* The labels of the field a text fills: its field frame's name
|
|
949
|
+
* (field/Caller ID), a Field instance's label, and its own layer name.
|
|
950
|
+
*/
|
|
951
|
+
function fieldLabels(a, n) {
|
|
952
|
+
const labels = [];
|
|
953
|
+
if (n.name && n.name !== n.characters) labels.push(n.name);
|
|
954
|
+
for (const x of a.ancestors(n).slice(0, 4)) {
|
|
955
|
+
const m = FIELD_FRAME.exec(x.name ?? "");
|
|
956
|
+
if (m) labels.push(m[2]);
|
|
957
|
+
for (const [prop, v] of Object.entries(x.componentProperties ?? {}))
|
|
958
|
+
if (/^label\b/i.test(prop) && typeof v?.value === "string")
|
|
959
|
+
labels.push(v.value);
|
|
960
|
+
}
|
|
961
|
+
return labels;
|
|
962
|
+
}
|
|
963
|
+
/** A business contact field (support, caller id, sender, business address). */
|
|
964
|
+
function businessField(a, n) {
|
|
965
|
+
const labels = fieldLabels(a, n);
|
|
966
|
+
return (
|
|
967
|
+
labels.some((l) => BUSINESS_FIELD.test(l)) &&
|
|
968
|
+
!labels.some((l) => PERSON_FIELD.test(l))
|
|
969
|
+
);
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
/**
|
|
973
|
+
* The node a library part's main draws, by default, where `node` sits in
|
|
974
|
+
* slot content: the nearest slot above it, in the instance that owns that
|
|
975
|
+
* slot, matched by the layer names from the slot down. Null when the node
|
|
976
|
+
* isn't in slot content, the owner's main can't be read, or its default
|
|
977
|
+
* content has no such layer.
|
|
978
|
+
*/
|
|
979
|
+
function defaultSlotContent(a, mains, node) {
|
|
980
|
+
const up = a.ancestors(node);
|
|
981
|
+
const slotAt = up.findIndex((x) => x.type === "SLOT");
|
|
982
|
+
if (slotAt === -1) return null;
|
|
983
|
+
const slot = up[slotAt];
|
|
984
|
+
const owner = up.slice(slotAt + 1).find((x) => x.type === "INSTANCE");
|
|
985
|
+
const tree = owner ? a.mainTree(owner.componentId) : null;
|
|
986
|
+
if (!owner || !tree) return null;
|
|
987
|
+
const main = mains[owner.componentId];
|
|
988
|
+
const names = [...up.slice(0, slotAt).reverse(), node].map((x) => x.name);
|
|
989
|
+
let level = [...tree.byId.values()].filter(
|
|
990
|
+
(x) => x.type === "SLOT" && x.name === slot.name,
|
|
991
|
+
);
|
|
992
|
+
for (const name of names)
|
|
993
|
+
level = level.flatMap((x) =>
|
|
994
|
+
(x.children ?? []).filter((c) => c.name === name),
|
|
995
|
+
);
|
|
996
|
+
return level[0] ? { node: level[0], styles: main.styles ?? {} } : null;
|
|
764
997
|
}
|
|
765
998
|
|
|
766
999
|
/**
|
|
@@ -865,7 +1098,12 @@ function handDrawnButton(n) {
|
|
|
865
1098
|
}
|
|
866
1099
|
function isSelf(a, n) {
|
|
867
1100
|
const up = a.ancestors(n);
|
|
868
|
-
if (
|
|
1101
|
+
if (
|
|
1102
|
+
up.some(
|
|
1103
|
+
(x) => SELF_CONTAINER.test(x.name ?? "") || SELF_MARK.test(x.name ?? ""),
|
|
1104
|
+
)
|
|
1105
|
+
)
|
|
1106
|
+
return true;
|
|
869
1107
|
const accountCard = (x) => {
|
|
870
1108
|
const set =
|
|
871
1109
|
x.type === "INSTANCE"
|