@aryacode/dsh-engineering 0.0.0-stage → 0.3.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.
Files changed (281) hide show
  1. package/CHANGELOG.md +115 -0
  2. package/LICENSE +21 -0
  3. package/README.md +228 -2
  4. package/cordis.patch.yml +122 -0
  5. package/locale/en.json +6 -0
  6. package/locale/zh.json +6 -0
  7. package/package.json +77 -3
  8. package/vendor/ftview/README.md +55 -0
  9. package/vendor/ftview/SKILL.md +74 -0
  10. package/vendor/ftview/lib/checks.d.ts +27 -0
  11. package/vendor/ftview/lib/checks.js +39 -0
  12. package/vendor/ftview/lib/core.d.ts +208 -0
  13. package/vendor/ftview/lib/core.js +160 -0
  14. package/vendor/ftview/lib/edit/drafts.d.ts +49 -0
  15. package/vendor/ftview/lib/edit/drafts.js +84 -0
  16. package/vendor/ftview/lib/edit/operations.d.ts +62 -0
  17. package/vendor/ftview/lib/edit/operations.js +126 -0
  18. package/vendor/ftview/lib/edit/prepare.d.ts +38 -0
  19. package/vendor/ftview/lib/edit/prepare.js +133 -0
  20. package/vendor/ftview/lib/edit/tools.d.ts +25 -0
  21. package/vendor/ftview/lib/edit/tools.js +198 -0
  22. package/vendor/ftview/lib/edit.d.ts +30 -0
  23. package/vendor/ftview/lib/edit.js +38 -0
  24. package/vendor/ftview/lib/index.d.ts +31 -0
  25. package/vendor/ftview/lib/index.js +36 -0
  26. package/vendor/ftview/lib/model.d.ts +80 -0
  27. package/vendor/ftview/lib/model.js +267 -0
  28. package/vendor/ftview/lib/session.d.ts +48 -0
  29. package/vendor/ftview/lib/session.js +98 -0
  30. package/vendor/ftview/lib/skill.d.ts +29 -0
  31. package/vendor/ftview/lib/skill.js +43 -0
  32. package/vendor/ftview/lib/studio/discover.d.ts +36 -0
  33. package/vendor/ftview/lib/studio/discover.js +87 -0
  34. package/vendor/ftview/lib/studio/preflight.d.ts +51 -0
  35. package/vendor/ftview/lib/studio/preflight.js +64 -0
  36. package/vendor/ftview/lib/studio/tools.d.ts +15 -0
  37. package/vendor/ftview/lib/studio/tools.js +83 -0
  38. package/vendor/ftview/lib/studio.d.ts +30 -0
  39. package/vendor/ftview/lib/studio.js +37 -0
  40. package/vendor/ftview/lib/tools.d.ts +14 -0
  41. package/vendor/ftview/lib/tools.js +202 -0
  42. package/vendor/l5x/README.md +62 -0
  43. package/vendor/l5x/SKILL.md +81 -0
  44. package/vendor/l5x/lib/approvals.d.ts +50 -0
  45. package/vendor/l5x/lib/approvals.js +93 -0
  46. package/vendor/l5x/lib/client.js +412 -0
  47. package/vendor/l5x/lib/client.js.map +1 -0
  48. package/vendor/l5x/lib/core/errors.d.ts +38 -0
  49. package/vendor/l5x/lib/core/errors.js +48 -0
  50. package/vendor/l5x/lib/core/params.d.ts +49 -0
  51. package/vendor/l5x/lib/core/params.js +39 -0
  52. package/vendor/l5x/lib/core/result.d.ts +46 -0
  53. package/vendor/l5x/lib/core/result.js +21 -0
  54. package/vendor/l5x/lib/core/tool.d.ts +102 -0
  55. package/vendor/l5x/lib/core/tool.js +19 -0
  56. package/vendor/l5x/lib/drafts.d.ts +66 -0
  57. package/vendor/l5x/lib/drafts.js +98 -0
  58. package/vendor/l5x/lib/editing.d.ts +30 -0
  59. package/vendor/l5x/lib/editing.js +42 -0
  60. package/vendor/l5x/lib/engine/acd-edit.d.ts +13 -0
  61. package/vendor/l5x/lib/engine/acd-edit.js +115 -0
  62. package/vendor/l5x/lib/engine/analysis.d.ts +22 -0
  63. package/vendor/l5x/lib/engine/analysis.js +258 -0
  64. package/vendor/l5x/lib/engine/capabilities.d.ts +9 -0
  65. package/vendor/l5x/lib/engine/capabilities.js +40 -0
  66. package/vendor/l5x/lib/engine/change-set.d.ts +122 -0
  67. package/vendor/l5x/lib/engine/change-set.js +446 -0
  68. package/vendor/l5x/lib/engine/configuration.d.ts +21 -0
  69. package/vendor/l5x/lib/engine/configuration.js +42 -0
  70. package/vendor/l5x/lib/engine/details.d.ts +47 -0
  71. package/vendor/l5x/lib/engine/details.js +173 -0
  72. package/vendor/l5x/lib/engine/direct-polling.d.ts +91 -0
  73. package/vendor/l5x/lib/engine/direct-polling.js +193 -0
  74. package/vendor/l5x/lib/engine/download-review.d.ts +15 -0
  75. package/vendor/l5x/lib/engine/download-review.js +63 -0
  76. package/vendor/l5x/lib/engine/logic-syntax.d.ts +60 -0
  77. package/vendor/l5x/lib/engine/logic-syntax.js +237 -0
  78. package/vendor/l5x/lib/engine/observations.d.ts +41 -0
  79. package/vendor/l5x/lib/engine/observations.js +67 -0
  80. package/vendor/l5x/lib/engine/project-analysis.d.ts +131 -0
  81. package/vendor/l5x/lib/engine/project-analysis.js +314 -0
  82. package/vendor/l5x/lib/engine/review-evidence.d.ts +8 -0
  83. package/vendor/l5x/lib/engine/review-evidence.js +49 -0
  84. package/vendor/l5x/lib/engine/review.d.ts +18 -0
  85. package/vendor/l5x/lib/engine/review.js +123 -0
  86. package/vendor/l5x/lib/engine/studio-sdk.d.ts +101 -0
  87. package/vendor/l5x/lib/engine/studio-sdk.js +571 -0
  88. package/vendor/l5x/lib/engine/tag-resolution.d.ts +20 -0
  89. package/vendor/l5x/lib/engine/tag-resolution.js +169 -0
  90. package/vendor/l5x/lib/engine/usage.d.ts +16 -0
  91. package/vendor/l5x/lib/engine/usage.js +92 -0
  92. package/vendor/l5x/lib/engine/xml-regions.d.ts +23 -0
  93. package/vendor/l5x/lib/engine/xml-regions.js +77 -0
  94. package/vendor/l5x/lib/fs.d.ts +17 -0
  95. package/vendor/l5x/lib/fs.js +36 -0
  96. package/vendor/l5x/lib/index.d.ts +36 -0
  97. package/vendor/l5x/lib/index.js +42 -0
  98. package/vendor/l5x/lib/live.d.ts +48 -0
  99. package/vendor/l5x/lib/live.js +48 -0
  100. package/vendor/l5x/lib/model/aois.d.ts +39 -0
  101. package/vendor/l5x/lib/model/aois.js +112 -0
  102. package/vendor/l5x/lib/model/errors.d.ts +15 -0
  103. package/vendor/l5x/lib/model/errors.js +18 -0
  104. package/vendor/l5x/lib/model/index.d.ts +20 -0
  105. package/vendor/l5x/lib/model/index.js +19 -0
  106. package/vendor/l5x/lib/model/module.d.ts +37 -0
  107. package/vendor/l5x/lib/model/module.js +54 -0
  108. package/vendor/l5x/lib/model/modules.d.ts +24 -0
  109. package/vendor/l5x/lib/model/modules.js +55 -0
  110. package/vendor/l5x/lib/model/parse.d.ts +32 -0
  111. package/vendor/l5x/lib/model/parse.js +116 -0
  112. package/vendor/l5x/lib/model/project.d.ts +15 -0
  113. package/vendor/l5x/lib/model/project.js +31 -0
  114. package/vendor/l5x/lib/model/routines.d.ts +71 -0
  115. package/vendor/l5x/lib/model/routines.js +183 -0
  116. package/vendor/l5x/lib/model/tags.d.ts +51 -0
  117. package/vendor/l5x/lib/model/tags.js +119 -0
  118. package/vendor/l5x/lib/model/types.d.ts +304 -0
  119. package/vendor/l5x/lib/model/types.js +12 -0
  120. package/vendor/l5x/lib/model/udts.d.ts +34 -0
  121. package/vendor/l5x/lib/model/udts.js +81 -0
  122. package/vendor/l5x/lib/model/xml.d.ts +139 -0
  123. package/vendor/l5x/lib/model/xml.js +291 -0
  124. package/vendor/l5x/lib/model/xref.d.ts +28 -0
  125. package/vendor/l5x/lib/model/xref.js +154 -0
  126. package/vendor/l5x/lib/session.d.ts +119 -0
  127. package/vendor/l5x/lib/session.js +236 -0
  128. package/vendor/l5x/lib/skill.d.ts +30 -0
  129. package/vendor/l5x/lib/skill.js +44 -0
  130. package/vendor/l5x/lib/studio-state.d.ts +77 -0
  131. package/vendor/l5x/lib/studio-state.js +79 -0
  132. package/vendor/l5x/lib/studio.d.ts +29 -0
  133. package/vendor/l5x/lib/studio.js +40 -0
  134. package/vendor/l5x/lib/tools/checks.d.ts +19 -0
  135. package/vendor/l5x/lib/tools/checks.js +140 -0
  136. package/vendor/l5x/lib/tools/common.d.ts +41 -0
  137. package/vendor/l5x/lib/tools/common.js +84 -0
  138. package/vendor/l5x/lib/tools/controller.d.ts +21 -0
  139. package/vendor/l5x/lib/tools/controller.js +170 -0
  140. package/vendor/l5x/lib/tools/drafts.d.ts +23 -0
  141. package/vendor/l5x/lib/tools/drafts.js +270 -0
  142. package/vendor/l5x/lib/tools/live.d.ts +21 -0
  143. package/vendor/l5x/lib/tools/live.js +175 -0
  144. package/vendor/l5x/lib/tools/read.d.ts +19 -0
  145. package/vendor/l5x/lib/tools/read.js +245 -0
  146. package/vendor/l5x/lib/tools/references.d.ts +20 -0
  147. package/vendor/l5x/lib/tools/references.js +269 -0
  148. package/vendor/l5x/lib/tools/studio.d.ts +21 -0
  149. package/vendor/l5x/lib/tools/studio.js +193 -0
  150. package/vendor/l5x/poll-worker.mjs +77 -0
  151. package/vendor/l5x/studio-bridge/NuGet.Config +1 -0
  152. package/vendor/l5x/studio-bridge/Program.cs +304 -0
  153. package/vendor/l5x/studio-bridge/StudioBridge.csproj +11 -0
  154. package/vendor/mwi/README.md +60 -0
  155. package/vendor/mwi/SKILL.md +87 -0
  156. package/vendor/mwi/bridge/README.md +8 -0
  157. package/vendor/mwi/bridge/mw_bridge.ps1 +2763 -0
  158. package/vendor/mwi/bridge/sdk-interop-importer.cs +30 -0
  159. package/vendor/mwi/bridge/sdk-variable-reader.cs +22 -0
  160. package/vendor/mwi/bridge/start_bridge.cmd +21 -0
  161. package/vendor/mwi/bridge/watch_dialogs.cmd +13 -0
  162. package/vendor/mwi/bridge/watch_dialogs.ps1 +153 -0
  163. package/vendor/mwi/code/engine/motionworks_iec_mcp/__init__.py +14 -0
  164. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/__init__.cpython-312.pyc +0 -0
  165. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/block_interfaces.cpython-312.pyc +0 -0
  166. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/cfb.cpython-312.pyc +0 -0
  167. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/datatypes.cpython-312.pyc +0 -0
  168. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/declarations.cpython-312.pyc +0 -0
  169. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/errors.cpython-312.pyc +0 -0
  170. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/graphical_listing.cpython-312.pyc +0 -0
  171. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/grid.cpython-312.pyc +0 -0
  172. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/knowledge.cpython-312.pyc +0 -0
  173. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/mwt_bind.cpython-312.pyc +0 -0
  174. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/navigation.cpython-312.pyc +0 -0
  175. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/program_checks.cpython-312.pyc +0 -0
  176. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/project.cpython-312.pyc +0 -0
  177. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/staging.cpython-312.pyc +0 -0
  178. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/structure.cpython-312.pyc +0 -0
  179. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/task_bindings.cpython-312.pyc +0 -0
  180. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/tasks.cpython-312.pyc +0 -0
  181. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/tree.cpython-312.pyc +0 -0
  182. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/validation.cpython-312.pyc +0 -0
  183. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/variable_edit.cpython-312.pyc +0 -0
  184. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/variables.cpython-312.pyc +0 -0
  185. package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/workflow.cpython-312.pyc +0 -0
  186. package/vendor/mwi/code/engine/motionworks_iec_mcp/block_interfaces.py +275 -0
  187. package/vendor/mwi/code/engine/motionworks_iec_mcp/build.py +339 -0
  188. package/vendor/mwi/code/engine/motionworks_iec_mcp/capture.py +448 -0
  189. package/vendor/mwi/code/engine/motionworks_iec_mcp/cfb.py +765 -0
  190. package/vendor/mwi/code/engine/motionworks_iec_mcp/com.py +268 -0
  191. package/vendor/mwi/code/engine/motionworks_iec_mcp/datatypes.py +204 -0
  192. package/vendor/mwi/code/engine/motionworks_iec_mcp/declarations.py +399 -0
  193. package/vendor/mwi/code/engine/motionworks_iec_mcp/descriptions.py +30 -0
  194. package/vendor/mwi/code/engine/motionworks_iec_mcp/eip.py +292 -0
  195. package/vendor/mwi/code/engine/motionworks_iec_mcp/errors.py +36 -0
  196. package/vendor/mwi/code/engine/motionworks_iec_mcp/graphical_listing.py +208 -0
  197. package/vendor/mwi/code/engine/motionworks_iec_mcp/grid.py +227 -0
  198. package/vendor/mwi/code/engine/motionworks_iec_mcp/ide.py +317 -0
  199. package/vendor/mwi/code/engine/motionworks_iec_mcp/iec.py +317 -0
  200. package/vendor/mwi/code/engine/motionworks_iec_mcp/installed_help.py +151 -0
  201. package/vendor/mwi/code/engine/motionworks_iec_mcp/knowledge.py +218 -0
  202. package/vendor/mwi/code/engine/motionworks_iec_mcp/libraries.py +250 -0
  203. package/vendor/mwi/code/engine/motionworks_iec_mcp/manuals.py +520 -0
  204. package/vendor/mwi/code/engine/motionworks_iec_mcp/mwt_bind.py +133 -0
  205. package/vendor/mwi/code/engine/motionworks_iec_mcp/navigation.py +44 -0
  206. package/vendor/mwi/code/engine/motionworks_iec_mcp/pou_writer.py +1547 -0
  207. package/vendor/mwi/code/engine/motionworks_iec_mcp/program_checks.py +360 -0
  208. package/vendor/mwi/code/engine/motionworks_iec_mcp/project.py +704 -0
  209. package/vendor/mwi/code/engine/motionworks_iec_mcp/reference_data/catalog.json +672 -0
  210. package/vendor/mwi/code/engine/motionworks_iec_mcp/reference_data/patterns.json +86 -0
  211. package/vendor/mwi/code/engine/motionworks_iec_mcp/restore.py +208 -0
  212. package/vendor/mwi/code/engine/motionworks_iec_mcp/server.py +1913 -0
  213. package/vendor/mwi/code/engine/motionworks_iec_mcp/snapshot.py +138 -0
  214. package/vendor/mwi/code/engine/motionworks_iec_mcp/st_comments.py +25 -0
  215. package/vendor/mwi/code/engine/motionworks_iec_mcp/staging.py +179 -0
  216. package/vendor/mwi/code/engine/motionworks_iec_mcp/stlint.py +676 -0
  217. package/vendor/mwi/code/engine/motionworks_iec_mcp/structure.py +98 -0
  218. package/vendor/mwi/code/engine/motionworks_iec_mcp/task_bindings.py +68 -0
  219. package/vendor/mwi/code/engine/motionworks_iec_mcp/tasks.py +162 -0
  220. package/vendor/mwi/code/engine/motionworks_iec_mcp/transaction.py +157 -0
  221. package/vendor/mwi/code/engine/motionworks_iec_mcp/tree.py +563 -0
  222. package/vendor/mwi/code/engine/motionworks_iec_mcp/tree_writer.py +790 -0
  223. package/vendor/mwi/code/engine/motionworks_iec_mcp/uia.py +465 -0
  224. package/vendor/mwi/code/engine/motionworks_iec_mcp/validation.py +41 -0
  225. package/vendor/mwi/code/engine/motionworks_iec_mcp/variable_edit.py +183 -0
  226. package/vendor/mwi/code/engine/motionworks_iec_mcp/variables.py +472 -0
  227. package/vendor/mwi/code/engine/motionworks_iec_mcp/workflow.py +130 -0
  228. package/vendor/mwi/code/engine/motionworks_iec_mcp/writer.py +666 -0
  229. package/vendor/mwi/code/mw_code.py +1258 -0
  230. package/vendor/mwi/legacy/edit-session.js +50 -0
  231. package/vendor/mwi/legacy/fb-insertion.js +74 -0
  232. package/vendor/mwi/legacy/native-code.js +83 -0
  233. package/vendor/mwi/legacy/native-groups.js +75 -0
  234. package/vendor/mwi/legacy/native-structure.js +143 -0
  235. package/vendor/mwi/legacy/native-variables.js +77 -0
  236. package/vendor/mwi/legacy/pou-conversion.js +47 -0
  237. package/vendor/mwi/legacy/pou-package.js +66 -0
  238. package/vendor/mwi/lib/core/bridge.d.ts +71 -0
  239. package/vendor/mwi/lib/core/bridge.js +53 -0
  240. package/vendor/mwi/lib/core/code-engine.d.ts +47 -0
  241. package/vendor/mwi/lib/core/code-engine.js +109 -0
  242. package/vendor/mwi/lib/core/errors.d.ts +37 -0
  243. package/vendor/mwi/lib/core/errors.js +47 -0
  244. package/vendor/mwi/lib/core/params.d.ts +52 -0
  245. package/vendor/mwi/lib/core/params.js +46 -0
  246. package/vendor/mwi/lib/core/ps-bridge.d.ts +62 -0
  247. package/vendor/mwi/lib/core/ps-bridge.js +127 -0
  248. package/vendor/mwi/lib/core/result.d.ts +45 -0
  249. package/vendor/mwi/lib/core/result.js +20 -0
  250. package/vendor/mwi/lib/core/tool.d.ts +80 -0
  251. package/vendor/mwi/lib/core/tool.js +55 -0
  252. package/vendor/mwi/lib/edit/compare.d.ts +33 -0
  253. package/vendor/mwi/lib/edit/compare.js +70 -0
  254. package/vendor/mwi/lib/edit.d.ts +20 -0
  255. package/vendor/mwi/lib/edit.js +32 -0
  256. package/vendor/mwi/lib/index.d.ts +21 -0
  257. package/vendor/mwi/lib/index.js +33 -0
  258. package/vendor/mwi/lib/read/service.d.ts +117 -0
  259. package/vendor/mwi/lib/read/service.js +229 -0
  260. package/vendor/mwi/lib/read.d.ts +21 -0
  261. package/vendor/mwi/lib/read.js +33 -0
  262. package/vendor/mwi/lib/session/project.d.ts +34 -0
  263. package/vendor/mwi/lib/session/project.js +54 -0
  264. package/vendor/mwi/lib/session/service.d.ts +167 -0
  265. package/vendor/mwi/lib/session/service.js +566 -0
  266. package/vendor/mwi/lib/session/stage.d.ts +84 -0
  267. package/vendor/mwi/lib/session/stage.js +187 -0
  268. package/vendor/mwi/lib/session/workspace.d.ts +52 -0
  269. package/vendor/mwi/lib/session/workspace.js +90 -0
  270. package/vendor/mwi/lib/skill.d.ts +36 -0
  271. package/vendor/mwi/lib/skill.js +56 -0
  272. package/vendor/mwi/lib/tools/edit.d.ts +20 -0
  273. package/vendor/mwi/lib/tools/edit.js +286 -0
  274. package/vendor/mwi/lib/tools/read.d.ts +19 -0
  275. package/vendor/mwi/lib/tools/read.js +161 -0
  276. package/vendor/mwi/lib/tools/session.d.ts +20 -0
  277. package/vendor/mwi/lib/tools/session.js +166 -0
  278. package/vendor/mwi/lib/tools/verify.d.ts +19 -0
  279. package/vendor/mwi/lib/tools/verify.js +85 -0
  280. package/vendor/mwi/lib/verify.d.ts +17 -0
  281. package/vendor/mwi/lib/verify.js +29 -0
@@ -0,0 +1,472 @@
1
+ """Parse MotionWorks IEC variable declarations.
2
+
3
+ A MotionWorks POU stores its variables in two places inside ``src.st1``:
4
+
5
+ ``<POU>V.VB``
6
+ The authoritative *textual* IEC 61131-3 declarations. This is what the
7
+ MotionWorks variable grid is showing you, and it carries the names, types,
8
+ addresses, and ``(*...*)`` descriptions. It is plain text and it is
9
+ complete, so it is the primary source of truth here.
10
+
11
+ ``<POU>V.VGR``
12
+ A binary "variable grid" record table holding the grid's UI state (handles,
13
+ row positions, usage flags, group assignment). It duplicates some of what
14
+ ``.VB`` holds and adds editor metadata.
15
+
16
+ Format of ``.VB`` (observed across all 34 declaration streams in the sample
17
+ projects)::
18
+
19
+ <blank>
20
+ (*Group:Default*)
21
+ <blank>
22
+ <blank>
23
+ VAR_EXTERNAL
24
+ \tTopCutter :\tAXIS_REF;(*SGD7S - 1 (Do Not Modify!!) *)<CRLF>
25
+ \tEIP_FromCLX_Axis_SVON_Cmd :\tBOOL;<CRLF>
26
+ END_VAR
27
+ <blank>
28
+ <blank>
29
+ VAR
30
+ \tMC_TopCutter_ServoOn :\tMC_Power;<CRLF>
31
+ END_VAR
32
+
33
+ Only ``VAR`` and ``VAR_EXTERNAL`` appear in POU files; the resource-level
34
+ ``Global_Variables.VB`` uses ``VAR_GLOBAL``. Blocks with the same keyword may
35
+ repeat, each optionally preceded by its own ``(*Group:...*)`` marker, so a
36
+ variable's group is the most recent marker seen at or before it.
37
+
38
+ Grammar per declaration::
39
+
40
+ <name> [AT <address>] : <type> ; [(*description*)]
41
+ """
42
+
43
+ from __future__ import annotations
44
+
45
+ import re
46
+ import struct
47
+ from dataclasses import dataclass, field
48
+
49
+ from .errors import UnsupportedFormat
50
+
51
+ #: Block keywords that may introduce a declaration list.
52
+ BLOCK_KEYWORDS = (
53
+ "VAR_GLOBAL",
54
+ "VAR_EXTERNAL",
55
+ "VAR_INPUT",
56
+ "VAR_OUTPUT",
57
+ "VAR_IN_OUT",
58
+ "VAR_TEMP",
59
+ "VAR_RETAIN",
60
+ "VAR_CONFIG",
61
+ "VAR_ACCESS",
62
+ "VAR",
63
+ )
64
+
65
+ _GROUP_RE = re.compile(
66
+ r"^\(\*Group\s*:\s*(?P<inner>.*)\*\)\s*$",
67
+ re.IGNORECASE | re.DOTALL,
68
+ )
69
+ _TRAILING_COMMENT_RE = re.compile(r"\(\*(.*?)\*\)\s*$", re.DOTALL)
70
+ _ADDRESS_RE = re.compile(r"^(.*?)\s+AT\s+(\S+)$", re.IGNORECASE | re.DOTALL)
71
+
72
+ #: Keywords that may follow a block keyword without introducing a declaration.
73
+ #: ``VAR_GLOBAL RETAIN`` is real MotionWorks output; ``RETAIN`` is a modifier,
74
+ #: not a variable name.
75
+ _BLOCK_MODIFIERS = {
76
+ "RETAIN", "NON_RETAIN", "CONSTANT", "PERSISTENT", "VAR_INPUT", "VAR_OUTPUT",
77
+ }
78
+
79
+ #: Usage values observed in the .VGR binary grid, for cross-checking.
80
+ USAGE_LOCAL = 1
81
+ USAGE_EXTERNAL = 5
82
+ USAGE_FB_INSTANCE = 0x00040001
83
+ USAGE_NAMES = {
84
+ USAGE_LOCAL: "local",
85
+ USAGE_EXTERNAL: "external",
86
+ USAGE_FB_INSTANCE: "fb-instance",
87
+ }
88
+
89
+ #: Block keyword -> whether it declares data owned by this POU.
90
+ _EXTERNAL_BLOCKS = {"VAR_EXTERNAL"}
91
+
92
+
93
+ @dataclass
94
+ class Variable:
95
+ """A single declared variable."""
96
+
97
+ name: str
98
+ type_name: str
99
+ section: str
100
+ group: str | None = None
101
+ address: str | None = None
102
+ description: str | None = None
103
+ line: int = 0
104
+ initial_value: str | None = None
105
+ disabled: bool = False
106
+
107
+ @property
108
+ def is_external(self) -> bool:
109
+ """True when the POU references a symbol owned elsewhere."""
110
+ return self.section in _EXTERNAL_BLOCKS
111
+
112
+ @property
113
+ def is_global(self) -> bool:
114
+ return self.section == "VAR_GLOBAL"
115
+
116
+ @property
117
+ def base_type(self) -> str:
118
+ """The type with any array/string qualification reduced.
119
+
120
+ ``ARRAY [0..7] OF INT`` reduces to ``ARRAY`` and ``STRING[80]`` to
121
+ ``STRING``, so classification is not fooled by parameters.
122
+ """
123
+ text = self.type_name.strip()
124
+ upper = text.upper()
125
+ if upper.startswith("ARRAY"):
126
+ return "ARRAY"
127
+ if "[" in text:
128
+ text = text.split("[", 1)[0].strip()
129
+ if "." in text:
130
+ # A dotted type such as a library structure reference.
131
+ text = text.rsplit(".", 1)[-1].strip()
132
+ return text
133
+
134
+ @property
135
+ def is_fb_instance(self) -> bool:
136
+ """True when the type looks like a function-block instance.
137
+
138
+ MotionWorks marks these with usage ``0x00040001`` in the binary grid.
139
+ A function-block instance is a *stateful* instance rather than a plain
140
+ value, which matters when transplanting bodies between POUs. The check
141
+ is case-insensitive and ignores any default initialiser, because a
142
+ declaration such as ``BOOL := FALSE`` is a plain value.
143
+ """
144
+ return (
145
+ not self.is_global
146
+ and not self.is_external
147
+ and self.base_type.upper() not in BUILTIN_TYPES
148
+ )
149
+
150
+ def to_dict(self) -> dict[str, object]:
151
+ return {
152
+ "name": self.name,
153
+ "type": self.type_name,
154
+ "section": self.section,
155
+ "group": self.group,
156
+ "address": self.address,
157
+ "initial_value": self.initial_value,
158
+ "description": self.description,
159
+ "line": self.line,
160
+ **({"disabled": True} if self.disabled else {}),
161
+ }
162
+
163
+ def format_line(self) -> str:
164
+ bits = [self.name]
165
+ if self.address:
166
+ bits.append(f"AT {self.address}")
167
+ text = f"{' '.join(bits)} : {self.type_name}"
168
+ if self.initial_value:
169
+ text += f" := {self.initial_value}"
170
+ if self.description:
171
+ text += f" (*{self.description}*)"
172
+ return text
173
+
174
+
175
+ @dataclass
176
+ class DeclarationTable:
177
+ """Every variable declared in one ``.VB`` stream."""
178
+
179
+ variables: list[Variable] = field(default_factory=list)
180
+ warnings: list[str] = field(default_factory=list)
181
+ source_stream: str | None = None
182
+
183
+ def __len__(self) -> int:
184
+ return len(self.variables)
185
+
186
+ def by_name(self, name: str) -> Variable | None:
187
+ target = name.casefold()
188
+ for v in self.variables:
189
+ if v.name.casefold() == target:
190
+ return v
191
+ return None
192
+
193
+ def sections(self) -> dict[str, int]:
194
+ counts: dict[str, int] = {}
195
+ for v in self.variables:
196
+ counts[v.section] = counts.get(v.section, 0) + 1
197
+ return counts
198
+
199
+ def duplicates(self) -> dict[str, list[Variable]]:
200
+ seen: dict[str, list[Variable]] = {}
201
+ for v in self.variables:
202
+ seen.setdefault(v.name.casefold(), []).append(v)
203
+ return {k: v for k, v in seen.items() if len(v) > 1}
204
+
205
+ def variants(self, name: str) -> list[Variable]:
206
+ """All declarations of ``name``, in file order.
207
+
208
+ MotionWorks permits the same symbol to appear in more than one block
209
+ (for example declared in a ``VAR`` and referenced in a
210
+ ``VAR_EXTERNAL``), so callers must not assume a name is unique.
211
+ """
212
+ target = name.casefold()
213
+ return [v for v in self.variables if v.name.casefold() == target]
214
+
215
+
216
+ #: IEC elementary types that are plain values rather than function-block
217
+ #: instances. Types deliberately *not* listed here (``AXIS_REF``,
218
+ #: ``TASK_INFO_ECLR``, ``CONTROLLER_INFO``, library structures) are reported as
219
+ #: instances, because classification only affects how a donor body is treated.
220
+ BUILTIN_TYPES = {
221
+ "BOOL", "BYTE", "WORD", "DWORD", "LWORD", "SINT", "USINT", "INT", "UINT",
222
+ "DINT", "UDINT", "LINT", "ULINT", "REAL", "LREAL", "STRING", "WSTRING",
223
+ "TIME", "LTIME", "DATE", "LDATE", "TOD", "LTOD", "DT", "LDT", "CHAR",
224
+ "WCHAR", "TIME_OF_DAY", "DATE_AND_TIME", "ARRAY",
225
+ }
226
+
227
+
228
+ def _split_type_initial_comment(body: str) -> tuple[str, str | None, str | None]:
229
+ """Split ``TYPE := INIT;(*desc*)`` into type, initial value, and description."""
230
+ description = None
231
+ match = _TRAILING_COMMENT_RE.search(body)
232
+ if match:
233
+ description = match.group(1).strip()
234
+ body = body[: match.start()]
235
+ body = body.strip().rstrip(";").strip()
236
+
237
+ # Native publication/PLC flags follow the initializer/type as brace attributes.
238
+ # Strip only these observed attributes outside strings; they are not IEC value
239
+ # text. Native flag read-back remains the authority for its boolean state.
240
+ attribute = re.search(r'\s*\{(?:PDD|CSV|NOP|RDT)(?:\s*,\s*(?:PDD|CSV|NOP|RDT))*\}\s*$', body, re.I)
241
+ if attribute:
242
+ quote = None
243
+ pos = 0
244
+ while pos < attribute.start():
245
+ char = body[pos]
246
+ if quote:
247
+ if char == '$': pos += 2; continue
248
+ if char == quote:
249
+ if pos+1 < attribute.start() and body[pos+1] == quote: pos += 2; continue
250
+ quote = None
251
+ elif char in "'\"": quote = char
252
+ pos += 1
253
+ if quote is None:
254
+ body = body[:attribute.start()].rstrip()
255
+
256
+ initial_value = None
257
+ if ":=" in body:
258
+ body, initial_value = body.split(":=", 1)
259
+ initial_value = initial_value.strip()
260
+ return body.strip(), initial_value, description
261
+
262
+
263
+ def parse_declarations(text: str, source_stream: str | None = None) -> DeclarationTable:
264
+ """Parse a ``.VB`` declaration stream into a :class:`DeclarationTable`.
265
+
266
+ The parser is tolerant: a line it cannot interpret becomes a warning rather
267
+ than an exception, because a partially understood file is still useful for
268
+ reading and a hard failure would hide the rest of the project.
269
+ """
270
+ table = DeclarationTable(source_stream=source_stream)
271
+ section: str | None = None
272
+ group: str | None = None
273
+
274
+ for lineno, raw_line in enumerate(text.splitlines(), start=1):
275
+ line = raw_line.strip()
276
+ disabled = False
277
+ # The IDE retains disabled declarations as (*<declaration;>*) followed
278
+ # by their description. Recognize that exact wrapper, not ordinary
279
+ # comments, and retain its disabled state for saved-source readers.
280
+ wrapped = re.fullmatch(r'\(\*<(.*?)>\*\)(\s*(?:\(\*.*?\*\))?)', line)
281
+ if wrapped:
282
+ line = wrapped[1] + wrapped[2]
283
+ disabled = True
284
+
285
+ group_match = _GROUP_RE.match(line)
286
+ if group_match:
287
+ group = group_match.group("inner").strip()
288
+ # A group marker is always alone on its line in practice, so once it
289
+ # is recognised the whole line is consumed.
290
+ continue
291
+
292
+ if not line:
293
+ continue
294
+
295
+ upper = line.upper()
296
+
297
+ if upper == "END_VAR":
298
+ section = None
299
+ continue
300
+
301
+ # Match the longest keyword first so VAR_EXTERNAL never matches VAR.
302
+ matched_block = None
303
+ for keyword in BLOCK_KEYWORDS:
304
+ if upper == keyword or upper.startswith(keyword + " "):
305
+ # Guard against "VAR_EXTERNAL" being caught by a prefix rule.
306
+ matched_block = keyword
307
+ break
308
+ if matched_block:
309
+ section = matched_block
310
+ remainder = line[len(matched_block):].strip()
311
+ if not remainder or remainder.upper().rstrip() in _BLOCK_MODIFIERS:
312
+ # "VAR_GLOBAL RETAIN" declares nothing on this line.
313
+ continue
314
+ line = remainder
315
+ upper = line.upper()
316
+
317
+ if section is None:
318
+ table.warnings.append(
319
+ f"line {lineno}: declaration outside any VAR block: {line!r}"
320
+ )
321
+ continue
322
+
323
+ if ":" not in line:
324
+ table.warnings.append(f"line {lineno}: no ':' in declaration: {line!r}")
325
+ continue
326
+
327
+ left, right = line.split(":", 1)
328
+ type_name, initial_value, description = _split_type_initial_comment(right)
329
+ if not type_name:
330
+ table.warnings.append(f"line {lineno}: empty type in {line!r}")
331
+ continue
332
+
333
+ left = left.strip()
334
+ address = None
335
+ address_match = _ADDRESS_RE.match(left)
336
+ if address_match:
337
+ left = address_match.group(1).strip()
338
+ address = address_match.group(2).strip()
339
+
340
+ if not left:
341
+ table.warnings.append(f"line {lineno}: empty variable name in {line!r}")
342
+ continue
343
+
344
+ # A left side such as "A, B" declares several names of one type.
345
+ for name in (n.strip() for n in left.split(",")):
346
+ if not name:
347
+ continue
348
+ table.variables.append(
349
+ Variable(
350
+ name=name,
351
+ type_name=type_name,
352
+ section=section,
353
+ group=group,
354
+ address=address,
355
+ description=description,
356
+ line=lineno,
357
+ initial_value=initial_value,
358
+ disabled=disabled,
359
+ )
360
+ )
361
+
362
+ if section is not None:
363
+ table.warnings.append(f"unterminated {section} block at end of file")
364
+
365
+ for name, dupes in table.duplicates().items():
366
+ table.warnings.append(
367
+ f"variable {name!r} declared {len(dupes)} times "
368
+ f"(lines {', '.join(str(d.line) for d in dupes)})"
369
+ )
370
+
371
+ return table
372
+
373
+
374
+ # Native record operations are centralized so duplicate definitions cannot diverge.
375
+ from .grid import parse as parse_grid_records, read_record as read_grid_record
376
+ from .grid import add as append_grid_variable, edit as update_grid_variable, delete as remove_grid_variable
377
+
378
+
379
+ def default_initial_value(type_name: str) -> str:
380
+ return {"BOOL": "FALSE", "REAL": "0.0", "LREAL": "0.0", "TIME": "T#0s"}.get(type_name.upper(), "0")
381
+
382
+
383
+ def read_grid_variable_count(grid: bytes) -> int | None:
384
+ """Return the variable count recorded in a ``.VGR`` binary grid header.
385
+
386
+ Offsets 0 and 4 hold format markers; the record count is a little-endian
387
+ DWORD at offset 8. This is used to cross-check the textual declaration
388
+ count, which is how a mis-parse or a desynchronised grid is detected.
389
+ """
390
+ if len(grid) < 12:
391
+ return None
392
+ return struct.unpack_from("<I", grid, 8)[0]
393
+
394
+
395
+ #: Magic observed at the start of MotionWorks' compressed stream container.
396
+ #: The body that follows is not a standard codec (zlib/deflate, LZMA and bzip2
397
+ #: all fail on it), so these streams are recognised and reported, not decoded.
398
+ COMPRESSED_MAGIC = bytes.fromhex("CADAC758")
399
+
400
+ #: Sidecar suffix holding a 64-byte digest plus an 8-byte header.
401
+ SIDECAR_SUFFIX = ".sn"
402
+
403
+
404
+ def is_compressed_stream(payload: bytes) -> bool:
405
+ """True when a stream uses the MotionWorks compressed container.
406
+
407
+ The magic is checked against the *whole* compact payload because the first
408
+ eight bytes are a little-endian hash/length header on some streams, which
409
+ shifts the magic off offset zero.
410
+ """
411
+ head = payload[:16]
412
+ return COMPRESSED_MAGIC in head or head[:4] == b"\x00\x00\x00\x00" and not looks_like_text(head)
413
+
414
+
415
+ def looks_like_text(payload: bytes) -> bool:
416
+ """Heuristic: is this payload plausibly IEC 61131-3 source text?"""
417
+ if not payload:
418
+ return True
419
+ sample = payload[:4096]
420
+ printable = sum(
421
+ 1 for b in sample if 32 <= b < 127 or b in (9, 10, 13)
422
+ )
423
+ return printable / len(sample) > 0.90
424
+
425
+
426
+ class CompressedStreamError(UnsupportedFormat):
427
+ """Raised when a declaration stream uses MotionWorks' compressed container.
428
+
429
+ Five of the largest RotaryKnife POUs serialise ``<POU>V.VB`` this way. The
430
+ format is not a recognised codec, so rather than guess (and risk writing
431
+ back a corrupted project) callers are told exactly what was found.
432
+ """
433
+
434
+
435
+ def decode_declarations(payload: bytes, source_stream: str | None = None) -> DeclarationTable:
436
+ """Decode a ``.VB`` payload, raising a precise error when it is compressed.
437
+
438
+ Kept separate from :func:`parse_declarations` so callers that already know
439
+ they hold text can skip the sniffing.
440
+ """
441
+ if is_compressed_stream(payload):
442
+ raise CompressedStreamError(
443
+ f"Declaration stream {source_stream or '<unnamed>'!r} uses the "
444
+ f"MotionWorks compressed container (magic {COMPRESSED_MAGIC.hex()}). "
445
+ f"This is not a standard codec and is not yet supported; the "
446
+ f"variables for this POU cannot be read or edited safely."
447
+ )
448
+ if not looks_like_text(payload):
449
+ raise UnsupportedFormat(
450
+ f"Declaration stream {source_stream or '<unnamed>'!r} is neither "
451
+ f"valid IEC text nor the known compressed container "
452
+ f"(first bytes: {payload[:8].hex(' ')})"
453
+ )
454
+ return parse_declarations(payload.decode("latin1"), source_stream=source_stream)
455
+
456
+
457
+ def cross_check(table: DeclarationTable, grid_count: int | None) -> list[str]:
458
+ """Compare the textual declaration count against the binary grid count.
459
+
460
+ A mismatch means the two stores disagree, which MotionWorks will surface as
461
+ inconsistent symbols. It is reported rather than raised so the read path
462
+ still returns useful data.
463
+ """
464
+ notes: list[str] = []
465
+ if grid_count is None:
466
+ return notes
467
+ if grid_count != len(table):
468
+ notes.append(
469
+ f"declaration count mismatch: .VB declares {len(table)} variables "
470
+ f"but the .VGR grid header records {grid_count}"
471
+ )
472
+ return notes
@@ -0,0 +1,130 @@
1
+ """Read-only readiness and source evidence; never repairs or restages a project."""
2
+ from __future__ import annotations
3
+
4
+ import hashlib
5
+ import json
6
+ import re
7
+ from pathlib import Path
8
+
9
+ from .cfb import CompoundFile
10
+ from .mwt_bind import embedded_paths
11
+ from .staging import assert_staged, assert_proven, assert_direct_attached, StagingRefused, workspace_root
12
+
13
+
14
+ def source_manifest(root: Path) -> dict:
15
+ """Hash real native sources, not generated DLLs or tmp.sto compiler caches."""
16
+ root = Path(root).resolve()
17
+ files = {}
18
+ streams = {}
19
+ for path in sorted(root.rglob('src.st1')):
20
+ if not path.resolve().is_relative_to(root):
21
+ raise ValueError(f'Linked source escapes project: {path}')
22
+ relative = path.relative_to(root).as_posix()
23
+ files[relative] = hashlib.sha256(path.read_bytes()).hexdigest()
24
+ cfb = CompoundFile(path)
25
+ streams[relative] = {
26
+ name: hashlib.sha256(cfb.read_stream(name)).hexdigest()
27
+ for name in sorted(cfb.stream_names())
28
+ }
29
+ for name in ('LIST.POU', 'PROJECT.INF'):
30
+ path = root / name
31
+ if path.is_file():
32
+ if not path.resolve().is_relative_to(root):
33
+ raise ValueError(f'Linked source escapes project: {path}')
34
+ files[name] = hashlib.sha256(path.read_bytes()).hexdigest()
35
+ # Description text lives outside src.st1. Losing a translation must fail
36
+ # persistence verification even when its binary description ID is unchanged.
37
+ translations = {}
38
+ for path in sorted(root.rglob('*Translation.xml')):
39
+ if not path.resolve().is_relative_to(root):
40
+ raise ValueError(f'Linked description escapes project: {path}')
41
+ relative = path.relative_to(root).as_posix()
42
+ translations[relative] = hashlib.sha256(path.read_bytes()).hexdigest()
43
+ files[relative] = translations[relative]
44
+ metadata = {name: value for name, value in files.items() if not name.endswith('src.st1')}
45
+ digest = hashlib.sha256(json.dumps({'streams': streams, 'metadata': metadata}, sort_keys=True).encode()).hexdigest()
46
+ # These two measured streams store expanded navigation-tree/view state.
47
+ # Keep their hashes as evidence, but do not confuse UI normalization with
48
+ # changed program sources. PROJECT.TRE is deliberately NOT excluded.
49
+ persistence_streams = {file: {name: value for name, value in store.items()
50
+ if name.upper() not in ('PRMVIEWALL.DAT', 'PRMVIEWHARD.DAT')}
51
+ for file, store in streams.items()}
52
+ persistence_digest = hashlib.sha256(json.dumps({'streams': persistence_streams,
53
+ 'metadata': metadata}, sort_keys=True).encode()).hexdigest()
54
+ programs = {file: {name: value for name, value in store.items()
55
+ if name.upper().endswith(('.VB', '.VGR', '.STB', '.GB', '.TXT'))}
56
+ for file, store in streams.items()}
57
+ program_digest = hashlib.sha256(json.dumps({'programs': programs, 'translations': translations}, sort_keys=True).encode()).hexdigest()
58
+ return {'project': str(root), 'source_digest': digest, 'files': files,
59
+ 'persistence_digest': persistence_digest,
60
+ 'program_digest': program_digest,
61
+ 'streams': streams, 'verification': 'native_source_only',
62
+ 'note': 'Source hashes are not compile, download, or motion evidence.'}
63
+
64
+
65
+ def check(project: str) -> dict:
66
+ from .project import Project
67
+ from .validation import validate
68
+
69
+ try:
70
+ root = assert_staged(project)
71
+ direct = False
72
+ except StagingRefused:
73
+ root = assert_direct_attached(project)
74
+ direct = True
75
+ if root.suffix.lower() == '.mwt':
76
+ root = root.with_suffix('')
77
+ # Diagnostic bypass is deliberately read-only, and still confines EVERY file
78
+ # to the stage. It is necessary to explain relocated identities, not trust them.
79
+ stage = workspace_root() / '.motionworks' / 'stage'
80
+ if (not direct and root.parent != stage.resolve()) or not root.is_dir():
81
+ raise ValueError('Select an authorized expanded workspace project')
82
+ for member in root.rglob('*'):
83
+ if not member.resolve().is_relative_to(root):
84
+ raise ValueError(f'Linked project member escapes stage: {member}')
85
+ blockers, warnings = [], []
86
+ try:
87
+ (assert_direct_attached if direct else assert_proven)(root)
88
+ identity_ok = True
89
+ except Exception as exc:
90
+ identity_ok = False
91
+ blockers.append({'code': 'identity_invalid', 'detail': str(exc),
92
+ 'next': 'Inspect the copied identity and original source. Do not restage over edits or hand-edit provenance.'})
93
+ wrapper = root.with_suffix('.mwt')
94
+ paths = []
95
+ try:
96
+ paths = embedded_paths(wrapper)
97
+ # Native sibling-directory wrappers legitimately store no absolute path.
98
+ # Use the same rule as check_mwt; do not manufacture a path for them.
99
+ bound = (all(Path(p).resolve() == root for p in paths) if paths else identity_ok)
100
+ if not bound:
101
+ blockers.append({'code': 'wrapper_unverified', 'detail': str(paths),
102
+ 'next': 'Check identity first, then mw_code_wrapper_binding. Preserve stage edits; do not restage over them.'})
103
+ except Exception as exc:
104
+ bound = False
105
+ blockers.append({'code': 'wrapper_unreadable', 'detail': str(exc),
106
+ 'next': 'Preserve the wrapper and inspect its native format.'})
107
+ validation = validate(root)
108
+ if not validation.get('ok'):
109
+ blockers.append({'code': 'native_validation_failed', 'detail': validation,
110
+ 'next': 'Inspect native errors before editing or opening.'})
111
+ p = Project(root)
112
+ bodies = {}
113
+ for pou in p.pous():
114
+ body = pou.st_body()
115
+ if body is not None:
116
+ bodies[pou.name] = body
117
+ timers = re.findall(r'PT\s*:=\s*(T#[\w.]+)', body, re.I)
118
+ if timers:
119
+ warnings.append({'code': 'timer_present', 'pou': pou.name,
120
+ 'detail': f'Native ST timer presets: {sorted(set(timers))}. Verify timing claims in notes against source.'})
121
+ return {'project': str(root), 'workspace': str(workspace_root()),
122
+ 'identity_valid': identity_ok, 'wrapper_bound': bound,
123
+ 'wrapper_paths': paths, 'ready_for_ide_open': not blockers,
124
+ 'binding_mode': 'embedded_path' if paths else 'native_sibling_directory',
125
+ 'blockers': blockers, 'warnings': warnings,
126
+ 'validation': validation, 'manifest': source_manifest(root),
127
+ 'st_pous': list(bodies), 'evidence_level': 'offline_only',
128
+ 'next_step': blockers[0]['next'] if blockers else
129
+ ('Continue the already-open directly attached project; verify Build, Make, errors and Save. Close/reopen only with consent.'
130
+ if direct else 'Open this exact wrapper; verify Build, Make, errors, save and reopen. Never download from this tool.')}