@aryacode/dsh-engineering 0.6.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.
- package/CHANGELOG.md +57 -0
- package/README.md +6 -4
- package/package.json +10 -11
- package/vendor/ftview/SKILL.md +34 -5
- package/vendor/ftview/lib/display-tree.d.ts +99 -0
- package/vendor/ftview/lib/display-tree.js +306 -0
- package/vendor/ftview/lib/edit/operations.d.ts +85 -1
- package/vendor/ftview/lib/edit/operations.js +201 -0
- package/vendor/ftview/lib/edit/prepare.d.ts +3 -0
- package/vendor/ftview/lib/edit/prepare.js +35 -1
- package/vendor/ftview/lib/edit/tools.js +249 -5
- package/vendor/ftview/lib/handoff.d.ts +58 -0
- package/vendor/ftview/lib/handoff.js +95 -0
- package/vendor/ftview/lib/index.d.ts +1 -0
- package/vendor/ftview/lib/index.js +3 -1
- package/vendor/ftview/lib/schema.d.ts +68 -0
- package/vendor/ftview/lib/schema.js +156 -0
- package/vendor/ftview/lib/vendor-tools.d.ts +17 -0
- package/vendor/ftview/lib/vendor-tools.js +74 -0
- package/vendor/ftview/lib/vendor.d.ts +56 -0
- package/vendor/ftview/lib/vendor.js +143 -0
- package/vendor/l5x/SKILL.md +27 -7
- package/vendor/l5x/lib/engine/change-set.d.ts +9 -1
- package/vendor/l5x/lib/engine/change-set.js +32 -7
- package/vendor/l5x/lib/engine/documentation.d.ts +58 -0
- package/vendor/l5x/lib/engine/documentation.js +237 -0
- package/vendor/l5x/lib/engine/instructions.d.ts +50 -0
- package/vendor/l5x/lib/engine/instructions.js +107 -0
- package/vendor/l5x/lib/engine/ladder.d.ts +74 -0
- package/vendor/l5x/lib/engine/ladder.js +209 -0
- package/vendor/l5x/lib/index.d.ts +1 -0
- package/vendor/l5x/lib/index.js +3 -1
- package/vendor/l5x/lib/model/routines.js +6 -1
- package/vendor/l5x/lib/session.d.ts +20 -0
- package/vendor/l5x/lib/session.js +36 -0
- package/vendor/l5x/lib/tools/document.d.ts +18 -0
- package/vendor/l5x/lib/tools/document.js +119 -0
- package/vendor/l5x/lib/tools/drafts.js +109 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,62 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.8.0] - 2026-10-27
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- **FactoryTalk View ME authoring.** The surface no longer only edits an export someone handed over:
|
|
8
|
+
- `ftview_apps` lists the ME applications installed on the host through the vendor export server
|
|
9
|
+
(74 applications were listed on the commissioning host).
|
|
10
|
+
- `ftview_export` exports one application to a NEW folder through `MEExportSvr` — no Studio window,
|
|
11
|
+
no project change. Proven end to end: 19 files and 13 displays for `Carpenter_EPS_Laminator001`.
|
|
12
|
+
- `ftview_draft_display_create`, `ftview_draft_object_add`, `ftview_draft_object_set`,
|
|
13
|
+
`ftview_draft_object_delete` and `ftview_draft_object_order` author displays and objects against a
|
|
14
|
+
source-positioned tree that keeps nesting (groups), connections and states.
|
|
15
|
+
- **Vendor-schema validation.** Every changed display is validated against the installed
|
|
16
|
+
`Gfx-ME*.xsd` through .NET before it can be applied; `ftview_apply` refuses a document the schema
|
|
17
|
+
rejects (`SCHEMA_INVALID`). All 13 real displays in the Laminator export validate with zero errors.
|
|
18
|
+
- `ftview_apply` now writes `Displays/BatchImport_<app>.xml` and `IMPORT.md`, the vendor's own
|
|
19
|
+
one-step import list for Studio's Batch Import.
|
|
20
|
+
- `ftview_verify_import` re-exports the application through the vendor engine and compares it object
|
|
21
|
+
by object with the draft, listing differing attributes and missing/extra objects.
|
|
22
|
+
|
|
23
|
+
### Documentation
|
|
24
|
+
|
|
25
|
+
- `packages/ftview/SKILL.md`: an Authoring section (apps → export → author → validate) and an Import
|
|
26
|
+
and verify section that states plainly which steps remain Studio and ME Transfer Utility.
|
|
27
|
+
- README: 97 tools, FactoryTalk View ME 22, and the 0.8.0 milestone.
|
|
28
|
+
## [0.7.0] - 2026-10-27
|
|
29
|
+
|
|
30
|
+
### Added
|
|
31
|
+
|
|
32
|
+
- **Ladder generation that cannot emit invalid NeutralText.** Three pieces:
|
|
33
|
+
- `list_instructions` — the instruction catalog the composer validates: mnemonic, category, summary
|
|
34
|
+
and operand slots with their kinds (BOOL, NUMBER, TIMER, …), filterable. 41 instructions, covering
|
|
35
|
+
every mnemonic the Laminator project uses (45 distinct across 860 rungs).
|
|
36
|
+
- `compose_rung` — builds one rung from a structure tree (`spec_json`) or a named pattern
|
|
37
|
+
(`start_stop_latch`, `fault_latch`, `ton_delay`, `interlock`), resolves every operand against the
|
|
38
|
+
export (including unqualified program tags and aliases), type-checks each slot, emits the export's
|
|
39
|
+
own bracket style, and re-parses its output with the same parser `draft_rung` uses. Unknown
|
|
40
|
+
instructions name the nearest catalog entries; a wrong operand count, a non-BOOL contact, a
|
|
41
|
+
non-numeric value slot, an undeclared tag or an undeclared AOI is refused before anything is staged.
|
|
42
|
+
Passing `program`, `routine`, `mode` and `rung` stages the rung in the draft; `fault_latch` stages
|
|
43
|
+
its two rungs consecutively.
|
|
44
|
+
- **Rung comments are read and written.** The model read the `Comment` **attribute** only, so a real
|
|
45
|
+
Studio export's 572 child `<Comment>` elements were invisible. `decodeRung` now supports both shapes,
|
|
46
|
+
`draft_rung` accepts a `comment`, and `draft_rung_comment` sets or clears one on an existing rung.
|
|
47
|
+
CDATA terminators in a comment round-trip through the existing escaping.
|
|
48
|
+
- **Documentation dimension.** `document_project` writes a handover bundle into a NEW folder and never
|
|
49
|
+
touches the export: `PROJECT.md` (controller, revision, tasks, programs, counts), `io.md`/`io.csv`,
|
|
50
|
+
`tags.csv` and `tags-studio.csv` (the `Tag Name,Description` shape Studio's Tags and Comments import
|
|
51
|
+
expects), and `rungs/<program>.md` with each rung's number, comment, NeutralText, instructions and
|
|
52
|
+
resolved tags. Filters: `include`, `programs`, `routines`, `rungs_per_file`.
|
|
53
|
+
|
|
54
|
+
### Documentation
|
|
55
|
+
|
|
56
|
+
- `packages/l5x/SKILL.md`: a Generate section (catalog → compose → validate → apply) and a
|
|
57
|
+
Documentation section (bundle contents, the comment round trip, what the bundle cannot prove).
|
|
58
|
+
- README: 89 tools, L5X 44, and the 0.7.0 milestone.
|
|
59
|
+
|
|
3
60
|
## [0.6.0] - 2026-10-27
|
|
4
61
|
|
|
5
62
|
### Added
|
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
MotionWorks IEC and FactoryTalk Optix sit behind a single install, one row per surface. A row is the
|
|
5
5
|
switch: disable it and that plugin unloads, so its tool schemas leave the request.
|
|
6
6
|
|
|
7
|
-
`@aryacode/dsh-engineering` ·
|
|
7
|
+
`@aryacode/dsh-engineering` · 97 tools · 5 rows · 4 products · MIT
|
|
8
8
|
|
|
9
9
|
---
|
|
10
10
|
|
|
@@ -36,14 +36,14 @@ bundle.
|
|
|
36
36
|
|
|
37
37
|
| Product | Tools | Default | Needs |
|
|
38
38
|
|---|---|---|---|
|
|
39
|
-
| **Studio 5000 L5X** |
|
|
40
|
-
| **FactoryTalk View ME** |
|
|
39
|
+
| **Studio 5000 L5X** | 44 | all on | Node for offline reads; Studio 5000 + Logix Designer SDK for SDK work; a controller for live |
|
|
40
|
+
| **FactoryTalk View ME** | 22 | all on | Node for an export folder; FactoryTalk View Studio ME for discovery and the handoff |
|
|
41
41
|
| **MotionWorks IEC** | 24 | all on | MotionWorks IEC 3 Pro installed; verified against 1.19 |
|
|
42
42
|
| **FactoryTalk Optix** | 10 (3 need preview) | all on | Optix Studio installed; Tesseract for OCR |
|
|
43
43
|
|
|
44
44
|
Tool groups, by product:
|
|
45
45
|
|
|
46
|
-
- **L5X** —
|
|
46
|
+
- **L5X** — 13 read (`open_project` … `document_project`), 11 staged drafts (`draft_*`, `compose_rung`), 6 Logix
|
|
47
47
|
Designer SDK, 7 supervised controller, 7 live EtherNet/IP (2 reads, 2 identity/discovery, 1 batch read, 2 approval-gated writes)
|
|
48
48
|
- **FactoryTalk View** — 6 read, 5 staged edits, 3 Studio handoff
|
|
49
49
|
- **MotionWorks IEC** — 7 session, 7 saved-project reads, 3 verify, 7 direct edits
|
|
@@ -120,6 +120,8 @@ the lean profile is the one that turns them off.
|
|
|
120
120
|
|
|
121
121
|
| Version | What landed |
|
|
122
122
|
|---|---|
|
|
123
|
+
| **0.8.0** | FactoryTalk View authoring: export through the vendor engine, author displays and objects in a schema-validated tree, stage a `BatchImport` handoff, and verify the import object by object |
|
|
124
|
+
| **0.7.0** | Ladder generation that cannot emit invalid NeutralText: an instruction catalog, a pattern/tree composer that resolves and type-checks every operand, rung comments read and written, and a documentation bundle (`document_project`) with the Studio tags-and-comments CSV |
|
|
123
125
|
| **0.5.0** | Commissioned on real files: FactoryTalk View reads genuine Studio exports (`<gfx>` displays, comma CSVs, UTF-16) instead of reporting zero; L5X alias resolution cuts 315 false "unresolved reference" findings to 16 on a 1279-tag project; `add_tag` no longer demands a value; a locked ACD exports from a copy |
|
|
124
126
|
| **0.4.17** | MotionWorks edits the real project again: the pre-edit backup is written where the bridge and engine verify it, an already-open project is attached instead of reloaded, and `mw_get` returns the body hash the edit takes |
|
|
125
127
|
| **0.4.15** | Every tool surface ships on by default; the row Configure pages switch them off |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aryacode/dsh-engineering",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "AryaCode engineering bundle: one install surface for the Studio 5000 L5X, FactoryTalk View ME, MotionWorks IEC and FactoryTalk Optix surfaces, one row per product, with the ladder and display previews.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"dsh",
|
|
@@ -55,15 +55,6 @@
|
|
|
55
55
|
]
|
|
56
56
|
}
|
|
57
57
|
},
|
|
58
|
-
"scripts": {
|
|
59
|
-
"test": "node --import tsx --test test/patch.spec.mjs test/docs.spec.mjs test/tool-reference.spec.mjs test/manifests.spec.mjs test/client-face.spec.mjs test/config.spec.mjs test/netlogic.spec.mjs",
|
|
60
|
-
"docs:tools": "tsx scripts/gen-tool-reference.ts",
|
|
61
|
-
"docs:check": "tsx scripts/gen-tool-reference.ts --check",
|
|
62
|
-
"netlogic:sync": "node scripts/sync-netlogic.mjs",
|
|
63
|
-
"build:client": "node scripts/build-client.mjs",
|
|
64
|
-
"assemble": "node scripts/assemble.mjs",
|
|
65
|
-
"prepack": "node scripts/build-client.mjs && node scripts/assemble.mjs"
|
|
66
|
-
},
|
|
67
58
|
"dependencies": {
|
|
68
59
|
"fast-xml-parser": "5.11.1",
|
|
69
60
|
"fast-xml-validator": "1.4.2",
|
|
@@ -103,5 +94,13 @@
|
|
|
103
94
|
"./mwi/skill": "./vendor/mwi/lib/skill.js",
|
|
104
95
|
"./mwi/verify": "./vendor/mwi/lib/verify.js",
|
|
105
96
|
"./mwi/edit": "./vendor/mwi/lib/edit.js"
|
|
97
|
+
},
|
|
98
|
+
"scripts": {
|
|
99
|
+
"test": "node --import tsx --test test/patch.spec.mjs test/docs.spec.mjs test/tool-reference.spec.mjs test/manifests.spec.mjs test/client-face.spec.mjs test/config.spec.mjs test/netlogic.spec.mjs",
|
|
100
|
+
"docs:tools": "tsx scripts/gen-tool-reference.ts",
|
|
101
|
+
"docs:check": "tsx scripts/gen-tool-reference.ts --check",
|
|
102
|
+
"netlogic:sync": "node scripts/sync-netlogic.mjs",
|
|
103
|
+
"build:client": "node scripts/build-client.mjs",
|
|
104
|
+
"assemble": "node scripts/assemble.mjs"
|
|
106
105
|
}
|
|
107
|
-
}
|
|
106
|
+
}
|
package/vendor/ftview/SKILL.md
CHANGED
|
@@ -65,11 +65,40 @@ unavailable, say editing is not enabled here rather than trying another tool.
|
|
|
65
65
|
- `ftview_draft_display_add` stages a new display by copying an existing display file.
|
|
66
66
|
- `ftview_draft_tag_upsert` stages one HMI tag description/type change or a new tag row.
|
|
67
67
|
- One operation per call. Each returns the draft id and the operation count; nothing is written yet.
|
|
68
|
-
- `ftview_validate` applies the staged edits to a re-parsed copy
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
68
|
+
- `ftview_validate` applies the staged edits to a re-parsed copy, checks the changed displays against
|
|
69
|
+
the installed `Gfx-ME*.xsd` and reports the changed files. `ftview_apply` refuses to write when the
|
|
70
|
+
vendor schema rejects a document.
|
|
71
|
+
- `ftview_apply` writes a NEW export folder, plus `Displays/BatchImport_<app>.xml` and `IMPORT.md` so
|
|
72
|
+
Studio's Batch Import has the exact list of changed displays. It never modifies the source.
|
|
73
|
+
|
|
74
|
+
## Authoring
|
|
75
|
+
|
|
76
|
+
Start from the application, not from a folder someone emailed:
|
|
77
|
+
|
|
78
|
+
1. `ftview_apps` lists the ME applications installed on this host.
|
|
79
|
+
2. `ftview_export` writes one to a NEW folder through the vendor export server. No Studio window is
|
|
80
|
+
needed and the application is not modified.
|
|
81
|
+
3. `ftview_open` that folder, then author against it:
|
|
82
|
+
- `ftview_draft_display_create` for a new display;
|
|
83
|
+
- `ftview_draft_object_add` for one object (`text`, `panel`, `momentaryButton`, `numericDisplay`,
|
|
84
|
+
`gotoButton`, …) with geometry, caption and tag connections;
|
|
85
|
+
- `ftview_draft_object_set`, `ftview_draft_object_delete`, `ftview_draft_object_order` for existing
|
|
86
|
+
objects.
|
|
87
|
+
4. `ftview_validate` must show `schema.ran: true` with no schema errors before you apply. `schema.ran:
|
|
88
|
+
false` means the host has no Studio schema and validation did not run; say so instead of claiming
|
|
89
|
+
validity.
|
|
90
|
+
|
|
91
|
+
## Import and verify
|
|
92
|
+
|
|
93
|
+
`ftview_apply` writes the folder, the manifest and `IMPORT.md`. Then:
|
|
94
|
+
|
|
95
|
+
1. A human opens the application in FactoryTalk View Studio and imports the displays listed in the
|
|
96
|
+
manifest.
|
|
97
|
+
2. `ftview_verify_import` exports the application again and compares it object by object with the
|
|
98
|
+
draft. `matched: true` for every display is the only proof the import landed; anything else lists
|
|
99
|
+
the differing attributes and objects.
|
|
100
|
+
3. Creating the runtime application (`.mer`) and downloading it to the terminal remain Studio and
|
|
101
|
+
ME Transfer Utility steps. No tool here creates a `.mer` or contacts a PanelView.
|
|
73
102
|
|
|
74
103
|
## Studio
|
|
75
104
|
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A source-positioned tree of one ME display.
|
|
3
|
+
*
|
|
4
|
+
* The flat object list was enough to report names, but authoring needs nesting
|
|
5
|
+
* (groups), connection elements and state blocks, and every edit needs to
|
|
6
|
+
* change only the bytes it owns. This scanner keeps element offsets in the
|
|
7
|
+
* original text, so an untouched object is never re-serialized.
|
|
8
|
+
*
|
|
9
|
+
* @module dsh-engineering-ftview/display-tree
|
|
10
|
+
*/
|
|
11
|
+
/** One element of a display document. */
|
|
12
|
+
export interface DisplayNode {
|
|
13
|
+
/** Element name as written, e.g. `momentaryButton`. */
|
|
14
|
+
readonly type: string;
|
|
15
|
+
/** Attributes in document order, values still entity-encoded. */
|
|
16
|
+
readonly attributes: Readonly<Record<string, string>>;
|
|
17
|
+
/** Children in document order. */
|
|
18
|
+
readonly children: readonly DisplayNode[];
|
|
19
|
+
/** Offset of the opening `<`. */
|
|
20
|
+
readonly start: number;
|
|
21
|
+
/** Offset just after the opening tag. */
|
|
22
|
+
readonly tagEnd: number;
|
|
23
|
+
/** Offset just after the final child, or `tagEnd` for an empty element. */
|
|
24
|
+
readonly contentEnd: number;
|
|
25
|
+
/** Offset just after the closing tag (or after `/>`). */
|
|
26
|
+
readonly end: number;
|
|
27
|
+
/** True when the element was written as `<x ... />`. */
|
|
28
|
+
readonly selfClosing: boolean;
|
|
29
|
+
}
|
|
30
|
+
/** One parsed display document. */
|
|
31
|
+
export interface DisplayTree {
|
|
32
|
+
/** The root element, `<gfx>` or `<display>`. */
|
|
33
|
+
readonly root: DisplayNode;
|
|
34
|
+
/** The original text. */
|
|
35
|
+
readonly xml: string;
|
|
36
|
+
/** Structural problems found while scanning. */
|
|
37
|
+
readonly issues: readonly string[];
|
|
38
|
+
}
|
|
39
|
+
/** Decode the five XML entities that can appear in an attribute value. */
|
|
40
|
+
export declare function decodeEntities(value: string): string;
|
|
41
|
+
/** Encode one attribute value for XML. */
|
|
42
|
+
export declare function encodeEntities(value: string): string;
|
|
43
|
+
/**
|
|
44
|
+
* Scan one display document into a tree.
|
|
45
|
+
*
|
|
46
|
+
* @param xml - Whole display file.
|
|
47
|
+
* @returns Root element, the source text and any structural issues.
|
|
48
|
+
*/
|
|
49
|
+
export declare function parseDisplayTree(xml: string): DisplayTree;
|
|
50
|
+
/** The raw text of one node. */
|
|
51
|
+
export declare function nodeText(tree: DisplayTree, node: DisplayNode): string;
|
|
52
|
+
/** Children of one type, in document order. */
|
|
53
|
+
export declare function childrenOfType(node: DisplayNode, type: string): readonly DisplayNode[];
|
|
54
|
+
/** The first child of one type. */
|
|
55
|
+
export declare function firstChildOfType(node: DisplayNode, type: string): DisplayNode | undefined;
|
|
56
|
+
/** Decoded attribute value, or undefined when absent. */
|
|
57
|
+
export declare function attributeOf(node: DisplayNode, name: string): string | undefined;
|
|
58
|
+
/** Depth-first walk. */
|
|
59
|
+
export declare function walk(node: DisplayNode, visit: (node: DisplayNode, depth: number) => void, depth?: number): void;
|
|
60
|
+
/** Every named object in the display, nested, in document order. */
|
|
61
|
+
export declare function namedObjects(root: DisplayNode): readonly DisplayNode[];
|
|
62
|
+
/**
|
|
63
|
+
* Set one attribute on a node, changing only its opening tag.
|
|
64
|
+
*
|
|
65
|
+
* @param tree - Parsed display.
|
|
66
|
+
* @param node - Target element.
|
|
67
|
+
* @param name - Attribute name.
|
|
68
|
+
* @param value - Plain value; entities are encoded here.
|
|
69
|
+
* @returns New document text.
|
|
70
|
+
*/
|
|
71
|
+
export declare function setAttribute(tree: DisplayTree, node: DisplayNode, name: string, value: string): string;
|
|
72
|
+
/** Remove one element from a document. */
|
|
73
|
+
export declare function removeNode(tree: DisplayTree, node: DisplayNode): string;
|
|
74
|
+
/**
|
|
75
|
+
* Insert an element inside a parent, at one child index.
|
|
76
|
+
*
|
|
77
|
+
* @param tree - Parsed display.
|
|
78
|
+
* @param parent - Parent element.
|
|
79
|
+
* @param childXml - Element text, already indented for its depth.
|
|
80
|
+
* @param index - Insert before this child; defaults to the end.
|
|
81
|
+
* @returns New document text.
|
|
82
|
+
*/
|
|
83
|
+
export declare function insertChild(tree: DisplayTree, parent: DisplayNode, childXml: string, index?: number): string;
|
|
84
|
+
/** Indentation prefix for one depth. */
|
|
85
|
+
export declare function indent(depth: number): string;
|
|
86
|
+
/**
|
|
87
|
+
* Render a new element.
|
|
88
|
+
*
|
|
89
|
+
* @param type - Element name.
|
|
90
|
+
* @param attributes - Attribute values, already encoded where needed.
|
|
91
|
+
* @param inner - Child markup, or undefined for an empty element.
|
|
92
|
+
* @param depth - Indentation depth.
|
|
93
|
+
* @returns Element text.
|
|
94
|
+
*/
|
|
95
|
+
export declare function renderElement(type: string, attributes: Readonly<Record<string, string>>, inner: string | undefined, depth: number): string;
|
|
96
|
+
/** Locate one named object anywhere in the tree. */
|
|
97
|
+
export declare function findNamed(root: DisplayNode, name: string): DisplayNode | undefined;
|
|
98
|
+
/** Path of one node, e.g. `gfx > group[Group1] > momentaryButton`. */
|
|
99
|
+
export declare function nodePath(node: DisplayNode): string;
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A source-positioned tree of one ME display.
|
|
3
|
+
*
|
|
4
|
+
* The flat object list was enough to report names, but authoring needs nesting
|
|
5
|
+
* (groups), connection elements and state blocks, and every edit needs to
|
|
6
|
+
* change only the bytes it owns. This scanner keeps element offsets in the
|
|
7
|
+
* original text, so an untouched object is never re-serialized.
|
|
8
|
+
*
|
|
9
|
+
* @module dsh-engineering-ftview/display-tree
|
|
10
|
+
*/
|
|
11
|
+
import { EngineeringError } from "./core.js";
|
|
12
|
+
/** Decode the five XML entities that can appear in an attribute value. */
|
|
13
|
+
export function decodeEntities(value) {
|
|
14
|
+
return value
|
|
15
|
+
.replaceAll('<', '<')
|
|
16
|
+
.replaceAll('>', '>')
|
|
17
|
+
.replaceAll('"', '"')
|
|
18
|
+
.replaceAll(''', "'")
|
|
19
|
+
.replaceAll(' ', '\n')
|
|
20
|
+
.replaceAll('
', '\n')
|
|
21
|
+
.replaceAll('&', '&');
|
|
22
|
+
}
|
|
23
|
+
/** Encode one attribute value for XML. */
|
|
24
|
+
export function encodeEntities(value) {
|
|
25
|
+
return value
|
|
26
|
+
.replaceAll('&', '&')
|
|
27
|
+
.replaceAll('<', '<')
|
|
28
|
+
.replaceAll('>', '>')
|
|
29
|
+
.replaceAll('"', '"')
|
|
30
|
+
.replaceAll('\n', ' ');
|
|
31
|
+
}
|
|
32
|
+
/** Scan one opening tag starting at `start`. */
|
|
33
|
+
function scanTag(xml, start) {
|
|
34
|
+
let cursor = start + 1;
|
|
35
|
+
const nameMatch = /^[A-Za-z_][\w.:-]*/u.exec(xml.slice(cursor));
|
|
36
|
+
if (nameMatch === null)
|
|
37
|
+
throw new EngineeringError('INVALID_DISPLAY', `Malformed element at offset ${start}.`, 'Open the file with ftview_get and fix the markup, or restore it from a backup.');
|
|
38
|
+
const name = nameMatch[0];
|
|
39
|
+
cursor += name.length;
|
|
40
|
+
const attributes = [];
|
|
41
|
+
while (cursor < xml.length) {
|
|
42
|
+
const character = xml[cursor];
|
|
43
|
+
if (character === '>') {
|
|
44
|
+
const before = xml.slice(start, cursor);
|
|
45
|
+
return { name, attributes, tagEnd: cursor + 1, selfClosing: /\/\s*$/u.test(before) };
|
|
46
|
+
}
|
|
47
|
+
if (/\s/u.test(character)) {
|
|
48
|
+
cursor += 1;
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
if (character === '/') {
|
|
52
|
+
cursor += 1;
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
const attributeStart = cursor;
|
|
56
|
+
const attributeName = /^[A-Za-z_][\w.:-]*/u.exec(xml.slice(cursor));
|
|
57
|
+
if (attributeName === null)
|
|
58
|
+
throw new EngineeringError('INVALID_DISPLAY', `Malformed attribute in <${name}> at offset ${cursor}.`, 'Fix the attribute name, then reopen the display.');
|
|
59
|
+
cursor += attributeName[0].length;
|
|
60
|
+
while (xml[cursor] === ' ' || xml[cursor] === '\t')
|
|
61
|
+
cursor += 1;
|
|
62
|
+
if (xml[cursor] !== '=') {
|
|
63
|
+
attributes.push({ name: attributeName[0], value: '', start: attributeStart, end: cursor });
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
cursor += 1;
|
|
67
|
+
while (xml[cursor] === ' ' || xml[cursor] === '\t')
|
|
68
|
+
cursor += 1;
|
|
69
|
+
const quote = xml[cursor];
|
|
70
|
+
if (quote !== '"' && quote !== "'")
|
|
71
|
+
throw new EngineeringError('INVALID_DISPLAY', `Unquoted value for ${attributeName[0]} in <${name}>.`, 'Quote every attribute value, then reopen the display.');
|
|
72
|
+
const valueStart = cursor + 1;
|
|
73
|
+
cursor = xml.indexOf(quote, valueStart);
|
|
74
|
+
if (cursor < 0)
|
|
75
|
+
throw new EngineeringError('INVALID_DISPLAY', `Unterminated value for ${attributeName[0]} in <${name}>.`, 'Close the quote, then reopen the display.');
|
|
76
|
+
attributes.push({ name: attributeName[0], value: xml.slice(valueStart, cursor), start: attributeStart, end: cursor + 1 });
|
|
77
|
+
cursor += 1;
|
|
78
|
+
}
|
|
79
|
+
throw new EngineeringError('INVALID_DISPLAY', `Unterminated <${name}> element.`, 'Fix the element, then reopen the display.');
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Scan one display document into a tree.
|
|
83
|
+
*
|
|
84
|
+
* @param xml - Whole display file.
|
|
85
|
+
* @returns Root element, the source text and any structural issues.
|
|
86
|
+
*/
|
|
87
|
+
export function parseDisplayTree(xml) {
|
|
88
|
+
const issues = [];
|
|
89
|
+
let root;
|
|
90
|
+
const stack = [];
|
|
91
|
+
let cursor = 0;
|
|
92
|
+
while (cursor < xml.length) {
|
|
93
|
+
const open = xml.indexOf('<', cursor);
|
|
94
|
+
if (open < 0)
|
|
95
|
+
break;
|
|
96
|
+
if (xml.startsWith('<!--', open)) {
|
|
97
|
+
const end = xml.indexOf('-->', open + 4);
|
|
98
|
+
if (end < 0) {
|
|
99
|
+
issues.push(`Unterminated comment at offset ${open}.`);
|
|
100
|
+
break;
|
|
101
|
+
}
|
|
102
|
+
cursor = end + 3;
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
if (xml.startsWith('<![CDATA[', open)) {
|
|
106
|
+
const end = xml.indexOf(']]>', open + 9);
|
|
107
|
+
if (end < 0) {
|
|
108
|
+
issues.push(`Unterminated CDATA at offset ${open}.`);
|
|
109
|
+
break;
|
|
110
|
+
}
|
|
111
|
+
cursor = end + 3;
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
if (xml.startsWith('<?', open)) {
|
|
115
|
+
const end = xml.indexOf('?>', open + 2);
|
|
116
|
+
if (end < 0) {
|
|
117
|
+
issues.push(`Unterminated processing instruction at offset ${open}.`);
|
|
118
|
+
break;
|
|
119
|
+
}
|
|
120
|
+
cursor = end + 2;
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
if (xml.startsWith('<!', open)) {
|
|
124
|
+
const end = xml.indexOf('>', open + 2);
|
|
125
|
+
if (end < 0) {
|
|
126
|
+
issues.push(`Unterminated declaration at offset ${open}.`);
|
|
127
|
+
break;
|
|
128
|
+
}
|
|
129
|
+
cursor = end + 1;
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
if (xml.startsWith('</', open)) {
|
|
133
|
+
const end = xml.indexOf('>', open + 2);
|
|
134
|
+
if (end < 0) {
|
|
135
|
+
issues.push(`Unterminated closing tag at offset ${open}.`);
|
|
136
|
+
break;
|
|
137
|
+
}
|
|
138
|
+
const name = xml.slice(open + 2, end).trim();
|
|
139
|
+
const current = stack.pop();
|
|
140
|
+
if (current === undefined || current.type !== name) {
|
|
141
|
+
issues.push(`Unexpected </${name}> at offset ${open}.`);
|
|
142
|
+
cursor = end + 1;
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
const node = {
|
|
146
|
+
type: current.type,
|
|
147
|
+
attributes: current.attributes,
|
|
148
|
+
children: current.children,
|
|
149
|
+
start: current.start,
|
|
150
|
+
tagEnd: current.tagEnd,
|
|
151
|
+
contentEnd: current.children.length === 0 ? current.tagEnd : current.children[current.children.length - 1].end,
|
|
152
|
+
end: end + 1,
|
|
153
|
+
selfClosing: false,
|
|
154
|
+
};
|
|
155
|
+
const parent = stack[stack.length - 1];
|
|
156
|
+
if (parent === undefined)
|
|
157
|
+
root = node;
|
|
158
|
+
else
|
|
159
|
+
parent.children.push(node);
|
|
160
|
+
cursor = end + 1;
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
const tag = scanTag(xml, open);
|
|
164
|
+
const attributes = {};
|
|
165
|
+
for (const attribute of tag.attributes)
|
|
166
|
+
attributes[attribute.name] = attribute.value;
|
|
167
|
+
if (tag.selfClosing) {
|
|
168
|
+
const node = { type: tag.name, attributes, children: [], start: open, tagEnd: tag.tagEnd, contentEnd: tag.tagEnd, end: tag.tagEnd, selfClosing: true };
|
|
169
|
+
const parent = stack[stack.length - 1];
|
|
170
|
+
if (parent === undefined)
|
|
171
|
+
root = node;
|
|
172
|
+
else
|
|
173
|
+
parent.children.push(node);
|
|
174
|
+
cursor = tag.tagEnd;
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
stack.push({ type: tag.name, attributes, children: [], start: open, tagEnd: tag.tagEnd, selfClosing: false });
|
|
178
|
+
cursor = tag.tagEnd;
|
|
179
|
+
}
|
|
180
|
+
if (stack.length > 0)
|
|
181
|
+
issues.push(`Unclosed element <${stack[stack.length - 1]?.type}>.`);
|
|
182
|
+
if (root === undefined)
|
|
183
|
+
throw new EngineeringError('INVALID_DISPLAY', 'The file has no root element.', 'Pass a FactoryTalk View display XML file.');
|
|
184
|
+
return { root, xml, issues };
|
|
185
|
+
}
|
|
186
|
+
/** The raw text of one node. */
|
|
187
|
+
export function nodeText(tree, node) {
|
|
188
|
+
return tree.xml.slice(node.start, node.end);
|
|
189
|
+
}
|
|
190
|
+
/** Children of one type, in document order. */
|
|
191
|
+
export function childrenOfType(node, type) {
|
|
192
|
+
return node.children.filter(child => child.type === type);
|
|
193
|
+
}
|
|
194
|
+
/** The first child of one type. */
|
|
195
|
+
export function firstChildOfType(node, type) {
|
|
196
|
+
return node.children.find(child => child.type === type);
|
|
197
|
+
}
|
|
198
|
+
/** Decoded attribute value, or undefined when absent. */
|
|
199
|
+
export function attributeOf(node, name) {
|
|
200
|
+
const raw = node.attributes[name];
|
|
201
|
+
return raw === undefined ? undefined : decodeEntities(raw);
|
|
202
|
+
}
|
|
203
|
+
/** Depth-first walk. */
|
|
204
|
+
export function walk(node, visit, depth = 0) {
|
|
205
|
+
visit(node, depth);
|
|
206
|
+
for (const child of node.children)
|
|
207
|
+
walk(child, visit, depth + 1);
|
|
208
|
+
}
|
|
209
|
+
/** Every named object in the display, nested, in document order. */
|
|
210
|
+
export function namedObjects(root) {
|
|
211
|
+
const found = [];
|
|
212
|
+
for (const child of root.children)
|
|
213
|
+
walk(child, node => { if (node.attributes.name !== undefined)
|
|
214
|
+
found.push(node); });
|
|
215
|
+
return found;
|
|
216
|
+
}
|
|
217
|
+
/** Serialize attributes with their original quoting. */
|
|
218
|
+
function renderAttributes(attributes) {
|
|
219
|
+
return Object.entries(attributes).map(([name, value]) => ` ${name}="${value}"`).join('');
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Set one attribute on a node, changing only its opening tag.
|
|
223
|
+
*
|
|
224
|
+
* @param tree - Parsed display.
|
|
225
|
+
* @param node - Target element.
|
|
226
|
+
* @param name - Attribute name.
|
|
227
|
+
* @param value - Plain value; entities are encoded here.
|
|
228
|
+
* @returns New document text.
|
|
229
|
+
*/
|
|
230
|
+
export function setAttribute(tree, node, name, value) {
|
|
231
|
+
const tag = tree.xml.slice(node.start, node.tagEnd);
|
|
232
|
+
const encoded = encodeEntities(value);
|
|
233
|
+
const pattern = new RegExp(`(\\s${name.replace(/[.*+?^${}()|[\]\\]/gu, '\\$&')}\\s*=\\s*)("[^"]*"|'[^']*')`, 'u');
|
|
234
|
+
if (pattern.test(tag)) {
|
|
235
|
+
const replaced = tag.replace(pattern, `$1"${encoded}"`);
|
|
236
|
+
return tree.xml.slice(0, node.start) + replaced + tree.xml.slice(node.tagEnd);
|
|
237
|
+
}
|
|
238
|
+
// Insert before the tag's closing '>' or '/>'.
|
|
239
|
+
const insertion = /(\/?)\s*>$/u.exec(tag);
|
|
240
|
+
if (insertion === null)
|
|
241
|
+
throw new EngineeringError('INVALID_DISPLAY', `Cannot extend <${node.type}>.`, 'Fix the opening tag, then retry.');
|
|
242
|
+
const at = tag.length - insertion[0].length;
|
|
243
|
+
const closing = insertion[1] === '/' ? '/>' : '>';
|
|
244
|
+
const rebuilt = `${tag.slice(0, at)} ${name}="${encoded}"${closing}`;
|
|
245
|
+
return tree.xml.slice(0, node.start) + rebuilt + tree.xml.slice(node.tagEnd);
|
|
246
|
+
}
|
|
247
|
+
/** Remove one element from a document. */
|
|
248
|
+
export function removeNode(tree, node) {
|
|
249
|
+
return tree.xml.slice(0, node.start) + tree.xml.slice(node.end);
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* Insert an element inside a parent, at one child index.
|
|
253
|
+
*
|
|
254
|
+
* @param tree - Parsed display.
|
|
255
|
+
* @param parent - Parent element.
|
|
256
|
+
* @param childXml - Element text, already indented for its depth.
|
|
257
|
+
* @param index - Insert before this child; defaults to the end.
|
|
258
|
+
* @returns New document text.
|
|
259
|
+
*/
|
|
260
|
+
export function insertChild(tree, parent, childXml, index) {
|
|
261
|
+
if (parent.selfClosing) {
|
|
262
|
+
const tag = tree.xml.slice(parent.start, parent.tagEnd);
|
|
263
|
+
const opened = tag.replace(/\/\s*>$/u, '>');
|
|
264
|
+
const rebuilt = `${opened}${childXml}</${parent.type}>`;
|
|
265
|
+
return tree.xml.slice(0, parent.start) + rebuilt + tree.xml.slice(parent.tagEnd);
|
|
266
|
+
}
|
|
267
|
+
const children = parent.children;
|
|
268
|
+
if (index === undefined || index >= children.length) {
|
|
269
|
+
// Append on a new line after the last child, not inside its own line.
|
|
270
|
+
const newline = tree.xml.indexOf('\n', parent.contentEnd);
|
|
271
|
+
const at = newline < 0 ? tree.xml.length : newline + 1;
|
|
272
|
+
return tree.xml.slice(0, at) + childXml + tree.xml.slice(at);
|
|
273
|
+
}
|
|
274
|
+
const at = children[Math.max(0, index)].start;
|
|
275
|
+
return tree.xml.slice(0, at) + childXml + tree.xml.slice(at);
|
|
276
|
+
}
|
|
277
|
+
/** Indentation prefix for one depth. */
|
|
278
|
+
export function indent(depth) {
|
|
279
|
+
return ' '.repeat(Math.max(0, depth));
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* Render a new element.
|
|
283
|
+
*
|
|
284
|
+
* @param type - Element name.
|
|
285
|
+
* @param attributes - Attribute values, already encoded where needed.
|
|
286
|
+
* @param inner - Child markup, or undefined for an empty element.
|
|
287
|
+
* @param depth - Indentation depth.
|
|
288
|
+
* @returns Element text.
|
|
289
|
+
*/
|
|
290
|
+
export function renderElement(type, attributes, inner, depth) {
|
|
291
|
+
const open = `${indent(depth)}<${type}${renderAttributes(attributes)}`;
|
|
292
|
+
return inner === undefined ? `${open}/>\n` : `${open}>\n${inner}${indent(depth)}</${type}>\n`;
|
|
293
|
+
}
|
|
294
|
+
/** Locate one named object anywhere in the tree. */
|
|
295
|
+
export function findNamed(root, name) {
|
|
296
|
+
let found;
|
|
297
|
+
walk(root, node => {
|
|
298
|
+
if (found === undefined && attributeOf(node, 'name') === name)
|
|
299
|
+
found = node;
|
|
300
|
+
});
|
|
301
|
+
return found;
|
|
302
|
+
}
|
|
303
|
+
/** Path of one node, e.g. `gfx > group[Group1] > momentaryButton`. */
|
|
304
|
+
export function nodePath(node) {
|
|
305
|
+
return node.type;
|
|
306
|
+
}
|