├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .npmignore ├── .travis.yml ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── ISSUE_TEMPLATE.md ├── LICENSE ├── PULL_REQUEST_TEMPLATE.md ├── README.md ├── manuals └── L5X.pdf ├── package-lock.json ├── package.json └── src ├── document ├── __snapshots__ │ └── document.spec.js.snap ├── __test-data__ │ └── datatype.L5X ├── document.spec.js └── index.js ├── element ├── __snapshots__ │ └── element.spec.js.snap ├── element.spec.js └── index.js ├── index.js ├── member ├── __snapshots__ │ └── member.spec.js.snap ├── index.js └── member.spec.js ├── program ├── __snapshots__ │ └── program.spec.js.snap ├── index.js └── program.spec.js ├── routine ├── __snapshots__ │ └── routine.spec.js.snap ├── index.js └── routine.spec.js ├── rung ├── __snapshots__ │ └── rung.spec.js.snap ├── index.js └── rung.spec.js ├── tag ├── __snapshots__ │ └── tag.spec.js.snap ├── index.js └── tag.spec.js └── utilities ├── index.js └── utilities.spec.js /.eslintignore: -------------------------------------------------------------------------------- 1 | 2 | node_modules/** 3 | coverage/** 4 | scripts/** -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | "jest/globals": true, 4 | es6: true, 5 | node: true 6 | }, 7 | extends: ["eslint:recommended"], 8 | parserOptions: { 9 | ecmaVersion: 8, 10 | sourceType: "module", 11 | ecmaFeatures: { 12 | jsx: true, 13 | experimentalObjectRestSpread: true 14 | } 15 | }, 16 | rules: { 17 | indent: ["error", 4], 18 | "no-console": 0, 19 | quotes: ["error", "double"], 20 | semi: ["error", "always"], 21 | "jest/no-disabled-tests": "warn", 22 | "jest/no-focused-tests": "error", 23 | "jest/no-identical-title": "error", 24 | "jest/prefer-to-have-length": "warn", 25 | "jest/valid-expect": "error" 26 | }, 27 | plugins: ["jest"] 28 | }; 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | 3 | /sample/ 4 | 5 | /scripts/ 6 | 7 | /assets/ 8 | 9 | /coverage/ -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | **/*.spec.js 2 | /coverage 3 | /manuals 4 | /scripts 5 | /sample 6 | /assets 7 | 8 | **/__snapshots__ 9 | **/__test-data__ 10 | CONTRIBUTE.md 11 | CODE_OF_CONDUCT.md 12 | ISSUE_TEMPLATE.md 13 | PULL_REQUEST_TEMPLATE.md -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "node" 4 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project Owner at cmseaton42@gmail.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contribute 2 | 3 | This is the **CONTRIBUTE** section of our project. Great to have you here. Here are a few ways you can help make this project better! 4 | 5 | ## Team Members 6 | 7 | *New Team Members Welcome...* 8 | 9 | * **Canaan Seaton** - *Owner* - [GitHub Profile](https://github.com/cmseaton42) - [Personal Website](http://www.canaanseaton.com/) 10 | 11 | ## Support 12 | 13 | How can I help? 14 | 15 | - Simply use the module. The more people who use it, the better it will become. 16 | - Get Involved (see below for more details) 17 | - A freshly dropped star on the repo is always appreciated ;) 18 | 19 | ## Learn & Listen 20 | 21 | This section includes ways to get started with your open source project. Include links to documentation and to different communication channels: 22 | 23 | * How to Open Source - See this excellent [article](https://opensource.guide/how-to-contribute/) 24 | * To Learn about CIP and Ethernet/IP - See the [Manuals](https://github.com/cmseaton42/L5XJS/tree/master/manuals) folder 25 | * Ask - See the project [Gitter](https://gitter.im/L5XJS/Lobby) community 26 | * Blog Posts - Coming Soon.... 27 | 28 | ## Get Involved 29 | 30 | So you say you wanna help? Here's how! 🎉 👍 31 | 32 | **Submitting Issues** 33 | 34 | Contributing to the Code is great but feedback on the project is just as important. 35 | 36 | 1. Research to make sure the issue isn't already being tracked 37 | 2. Submit your issue via the project [issue tracker](https://github.com/cmseaton42/L5XJS/issues) 38 | - Be Clear 39 | - Be Thorough (too much information >>> not enough information) 40 | - Include Pictures (Screenshots of Errors, gifs, etc) 41 | - Include *Node Version* -> `node --version` 42 | - Include *package version* -> `npm list` 43 | 44 | **Protip:** Use the project [Issue Template](https://github.com/cmseaton42/L5XJS/blob/master/ISSUE_TEMPLATE.md) as a starting point 45 | 46 | **Feature Requests** 47 | 48 | Have you ever been working with a project and thought *Man, I wish it just did this out of the box*? 49 | 50 | 1. Submit your feature request to the [issue tracker](https://github.com/cmseaton42/L5XJS/issues) **Be Sure to Mark your Issue as a Feature Request in the title (eg `[FEATURE REQUEST] Some Awesome Idea`) 51 | - Be Clear 52 | - Be Thorough (too much information >>> not enough information) 53 | - Don't submit it and *forget* it, be prepared to answer some follow up questions 54 | 55 | **Protip:** Use the project [Issue Template](https://github.com/cmseaton42/L5XJS/blob/master/ISSUE_TEMPLATE.md) as a starting point 56 | 57 | **Contributing Code** 58 | 59 | 1. Open an issue if one doesn't already partain to your contribution 60 | 2. Discuss the changes you want to make in the issue tracker to see if someone is already working on it or if there is a reason it shouldn't be added 61 | 1. Assumming the outcome of the above discussion good to go, fork the Repo 62 | 2. Clone your forked copy to your local machine 63 | 3. Add the host repo as upstream -> `git remote add upstream https://github.com/cmseaton42/L5XJS.git` 64 | 3. Get acclamated with the environment (ask questions if necessary) 65 | 5. Make a new branch for your changes 66 | 6. Start *hacking* away at some crispy new code 67 | 7. Make sure to add tests 68 | 8. Submit you're PR to the [Primary](https://github.com/cmseaton42/L5XJS) repo 69 | - **Protip:** Use the [Pull Request Template](https://github.com/cmseaton42/L5XJS/blob/master/PULL_REQUEST_TEMPLATE.md) as a starting point 70 | 9. Wait for feedback 71 | 10. Make changes if necessary and re-submit 72 | 11. Boom, PR accepted 👍 💯 🎉 73 | 74 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Current Behavior 4 | 5 | 6 | 7 | ## Expected Behavior 8 | 9 | 10 | 11 | ## Possible Solution (Optional) 12 | 13 | 14 | 15 | ## Context 16 | 17 | 18 | 19 | ## Steps to Reproduce (for bugs only) 20 | 21 | 22 | 1. 23 | 2. 24 | 3. 25 | 4. 26 | 27 | ## Your Environment 28 | 29 | * Package version (Use `npm list` - e.g. *1.0.6*): 30 | * Node Version (Use `node --version` - e.g. *9.8.0*): 31 | * Operating System and version: 32 | * Controller Type (eg 1756-L83E/B): 33 | * Controller Firmware (eg 30.11): 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2018 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 7 | of the Software, and to permit persons to whom the Software is furnished to do 8 | so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ### Description, Motivation, and Context 4 | 5 | 6 | 7 | ## How Has This Been Tested? 8 | 9 | 10 | 11 | 12 | ## Screenshots (if appropriate): 13 | 14 | ## Types of changes 15 | 16 | - [ ] Bug fix (non-breaking change which fixes an issue) 17 | - [ ] New feature (non-breaking change which adds functionality) 18 | - [ ] Breaking change (fix or feature that would cause existing functionality to change) 19 | 20 | ## Checklist: 21 | 22 | 23 | - [ ] My code follows the code style of this project. 24 | - [ ] My change requires a change to the documentation. 25 | - [ ] I have updated the documentation accordingly. 26 | - [ ] I have read the **CONTRIBUTING** document. 27 | - [ ] I have added tests to cover my changes. 28 | - [ ] All new and existing tests passed. 29 | - [ ] This is a work in progress, and I want some feedback (If yes, please mark it in the title -> e.g. `[WIP] Some awesome PR title`) 30 | 31 | ## Related Issue 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 |

L5XJS Logo

4 | 5 |
6 |

7 | npm 8 | Gitter 9 | license 10 | Travis 11 | Coveralls github 12 |

13 |
14 | 15 | # L5XJS 16 | 17 | A simple API designed to build and parse Allen Bradley *.L5X* files in order to empower users to automate PLC code generation. 18 | 19 | ## Prerequisites 20 | 21 | latest version of [NodeJS](https://nodejs.org/en/) 22 | 23 | ## Getting Started 24 | 25 | Install with npm 26 | 27 | ``` 28 | npm install l5x-js --save 29 | ``` 30 | ## The API 31 | 32 | How the heck does this thing work anyway? Great question! 33 | 34 | 35 | 36 | ## Built With 37 | 38 | * [NodeJS](https://nodejs.org/en/) - The Engine 39 | * [javascript - ES2017](https://maven.apache.org/) - The Language 40 | 41 | ## Contributers 42 | 43 | * **Canaan Seaton** - *Owner* - [GitHub Profile](https://github.com/cmseaton42) - [Personal Website](http://www.canaanseaton.com/) 44 | 45 | Wanna *become* a contributor? [Here's](https://github.com/cmseaton42/L5XJS/blob/master/CONTRIBUTING.md) how! 46 | 47 | ## License 48 | 49 | This project is licensed under the MIT License - see the [LICENCE](https://github.com/cmseaton42/L5XJS/blob/master/LICENSE) file for details 50 | -------------------------------------------------------------------------------- /manuals/L5X.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cmseaton42/L5XJS/7a81754fc7dfa6a090dc1703ef0d2f2e6d7b9456/manuals/L5X.pdf -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "l5x-js", 3 | "version": "1.0.0-beta.0", 4 | "description": "A tool to help with parsing and generating Allen Bradley L5X files.", 5 | "main": "./src/index.js", 6 | "scripts": { 7 | "test": "npm run lint && jest && npm run test:coverage && cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js", 8 | "test:local": "npm run lint && jest && npm run test:coverage", 9 | "test:watch": "jest --watch", 10 | "test:coverage": "jest --coverage", 11 | "test:detailed": "jest --verbose", 12 | "test:update": "jest -u", 13 | "lint": "./node_modules/.bin/eslint . --ext .js", 14 | "lint:fix": "npm run lint -- --fix" 15 | }, 16 | "keywords": [ 17 | "Allen", 18 | "Bradley", 19 | "Node", 20 | "javascript", 21 | "Allen-Bradley", 22 | "L5X", 23 | "L5K", 24 | "Generator", 25 | "Parser" 26 | ], 27 | "author": "Canaan Seaton", 28 | "license": "ISC", 29 | "dependencies": { 30 | "clone": "^1.0.4", 31 | "xml-js": "^1.6.2" 32 | }, 33 | "devDependencies": { 34 | "coveralls": "^3.0.0", 35 | "eslint": "^4.19.1", 36 | "eslint-plugin-jest": "^21.15.0", 37 | "jest": "^22.4.3" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/document/__test-data__/datatype.L5X: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 44 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /src/document/document.spec.js: -------------------------------------------------------------------------------- 1 | const Document = require("./index"); 2 | const Tag = require("../tag"); 3 | const Program = require("../program"); 4 | const Routine = require("../routine"); 5 | const fs = require("fs"); 6 | const path = require("path"); 7 | 8 | describe("Document Class", () => { 9 | describe("New Instance", () => { 10 | it("Accepts Proper Input", () => { 11 | const fn = arg => () => new Document(arg); 12 | 13 | expect(fn("./__test-data__/datatype.L5X")).toThrow(); 14 | expect(fn(57)).toThrow(); 15 | expect(fn(path.resolve(__dirname, "./__test-data__/datatype.L5X"))).not.toThrow(); 16 | expect(fn()).not.toThrow(); 17 | }); 18 | 19 | it("Initializes with Desired Document Model", () => { 20 | const doc1 = new Document(); 21 | const doc2 = new Document(path.resolve(__dirname, "./__test-data__/datatype.L5X")); 22 | 23 | expect(doc1._dom).toMatchSnapshot(); 24 | expect(doc2._dom).toMatchSnapshot(); 25 | }); 26 | }); 27 | 28 | describe("Static Methods", () => { 29 | test("isDocument: Returns Appropriate Judgement", () => { 30 | const doc = new Document(); 31 | const fake = {}; 32 | 33 | expect(Document.isDocument(doc)).toBeTruthy(); 34 | expect(Document.isDocument(fake)).toBeFalsy(); 35 | }); 36 | }); 37 | 38 | describe("Methods", () => { 39 | let docFull; 40 | let docEmpty; 41 | 42 | beforeEach(() => { 43 | docFull = new Document(path.resolve(__dirname, "./__test-data__/datatype.L5X")); 44 | docEmpty = new Document(); 45 | }); 46 | 47 | test("addTag: Throws on Invalid Input", () => { 48 | const fn = tag => () => docFull.addTag(tag); 49 | 50 | expect(fn(new Program("notATag"))).toThrow(); 51 | expect(fn("notATag")).toThrow(); 52 | }); 53 | 54 | test("addTag: Adds Tag to Document Instance", () => { 55 | docFull.addTag(new Tag("tag1", "BOOL")); 56 | docEmpty.addTag(new Tag("tag1", "BOOL")); 57 | expect(docFull).toMatchSnapshot(); 58 | expect(docEmpty).toMatchSnapshot(); 59 | 60 | docFull.addTag(new Tag("tag2", "DINT")); 61 | docEmpty.addTag(new Tag("tag2", "DINT")); 62 | expect(docFull).toMatchSnapshot(); 63 | expect(docEmpty).toMatchSnapshot(); 64 | 65 | docEmpty.addTag(new Tag("tag3", "SINT")); 66 | expect(docEmpty).toMatchSnapshot(); 67 | 68 | const tag4 = new Tag("tag4", "REAL"); 69 | docEmpty.addTag(tag4); 70 | expect(docEmpty).toMatchSnapshot(); 71 | 72 | tag4.dom.attributes.Name = "not_tag4Anymore"; 73 | expect(docEmpty).toMatchSnapshot(); 74 | }); 75 | 76 | test("addProgam: Throws on Invalid Input", () => { 77 | const fn = prog => () => docFull.addProgram(prog); 78 | 79 | expect(fn(new Program("notATag"))).not.toThrow(); 80 | expect(fn(new Tag("ATag", "DINT"))).toThrow(); 81 | expect(fn("notATag")).toThrow(); 82 | }); 83 | 84 | test("addProgram: Adds Program to Document Instance", () => { 85 | docFull.addProgram(new Program("aProgram")); 86 | docFull.addProgram(new Program("bProgram")); 87 | docFull.addProgram(new Program("cProgram")); 88 | 89 | docEmpty.addProgram(new Program("aProgram")); 90 | docEmpty.addProgram(new Program("bProgram")); 91 | docEmpty.addProgram(new Program("cProgram")); 92 | 93 | expect(docFull).toMatchSnapshot(); 94 | expect(docEmpty).toMatchSnapshot(); 95 | 96 | const prog = new Program("dProgram"); 97 | docEmpty.addProgram(prog); 98 | expect(docEmpty).toMatchSnapshot(); 99 | 100 | prog.addRoutine(new Routine("aRoutine")); 101 | expect(docEmpty).toMatchSnapshot(); 102 | }); 103 | 104 | test("findTag: Throws on Invalid Input", () => { 105 | const fn = name => () => docEmpty.findTag(name); 106 | 107 | expect(fn(new Program("notATag"))).toThrow(); 108 | expect(fn(12)).toThrow(); 109 | expect(fn("aTag")).not.toThrow(); 110 | }); 111 | 112 | test("findTag: Finds Target Tag", () => { 113 | docEmpty.addTag(new Tag("aTag", "DINT")); 114 | docEmpty.addTag(new Tag("bTag", "SINT")); 115 | 116 | const tag = new Tag("cTag", "BOOL"); 117 | docEmpty.addTag(tag); 118 | 119 | expect(docEmpty.findTag("aTag")).not.toBeNull(); 120 | expect(docEmpty.findTag("aTag")).toMatchSnapshot(); 121 | expect(docEmpty.findTag("bTag")).not.toBeNull(); 122 | expect(docEmpty.findTag("bTag")).toMatchSnapshot(); 123 | expect(docEmpty.findTag("cTag")).not.toBeNull(); 124 | expect(docEmpty.findTag("cTag")).toMatchSnapshot(); 125 | expect(docEmpty.findTag("dTag")).toBeNull(); 126 | 127 | docEmpty.findTag("cTag").dom.attributes.Name = "eTag"; 128 | expect(docEmpty).toMatchSnapshot(); 129 | expect(tag).toMatchSnapshot(); 130 | }); 131 | 132 | test("findProgram: Throws on Invalid Input", () => { 133 | const fn = name => () => docEmpty.findProgram(name); 134 | 135 | expect(fn(new Program("notATag"))).toThrow(); 136 | expect(fn(12)).toThrow(); 137 | expect(fn("aProgram")).not.toThrow(); 138 | }); 139 | 140 | test("findProgram: Finds Target Program", () => { 141 | docEmpty.addProgram(new Program("aProgram")); 142 | docEmpty.addProgram(new Program("bProgram")); 143 | 144 | const prog = new Program("cProgram"); 145 | docEmpty.addProgram(prog); 146 | 147 | expect(docEmpty.findProgram("aProgram")).not.toBeNull(); 148 | expect(docEmpty.findProgram("aProgram")).toMatchSnapshot(); 149 | expect(docEmpty.findProgram("bProgram")).not.toBeNull(); 150 | expect(docEmpty.findProgram("bProgram")).toMatchSnapshot(); 151 | expect(docEmpty.findProgram("cProgram")).not.toBeNull(); 152 | expect(docEmpty.findProgram("cProgram")).toMatchSnapshot(); 153 | expect(docEmpty.findProgram("dProgram")).toBeNull(); 154 | 155 | docEmpty.findProgram("cProgram").dom.attributes.Name = "eProgram"; 156 | expect(docEmpty).toMatchSnapshot(); 157 | expect(prog).toMatchSnapshot(); 158 | }); 159 | 160 | test("setTarget: Throws on Invalid Inputs", () => { 161 | const fn = target => () => docEmpty.setTarget(target); 162 | 163 | expect(fn()).toThrow(); 164 | expect(fn(12)).toThrow(); 165 | expect(fn(new Program("aProgram"))).not.toThrow(); 166 | expect(fn(new Tag("aTag", "DINT"))).toThrow(); 167 | }); 168 | 169 | test("setTarget: Sets Target Successfully", () => { 170 | const progA = new Program("aProgram"); 171 | const progB = new Program("bProgram"); 172 | const routA = new Routine("aRoutine"); 173 | const routB = new Routine("bRoutine"); 174 | 175 | docEmpty.addProgram(progA); 176 | progA.addRoutine(routA); 177 | 178 | docEmpty.addProgram(progB); 179 | progB.addRoutine(routB); 180 | 181 | docEmpty.setTarget(progA); 182 | expect(docEmpty).toMatchSnapshot(); 183 | 184 | docEmpty.setTarget(progB); 185 | expect(docEmpty).toMatchSnapshot(); 186 | 187 | docEmpty.setTarget(routA); 188 | expect(docEmpty).toMatchSnapshot(); 189 | 190 | docEmpty.setTarget(routB); 191 | expect(docEmpty).toMatchSnapshot(); 192 | }); 193 | 194 | test("export: Exported Data is Correct", () => { 195 | docFull.export(path.resolve(__dirname, "./__test-data__/generated_datatype.L5X")); 196 | 197 | let original = fs.readFileSync( 198 | path.resolve(__dirname, "./__test-data__/datatype.L5X"), 199 | "utf8" 200 | ); 201 | 202 | let generated = fs.readFileSync( 203 | path.resolve(__dirname, "./__test-data__/generated_datatype.L5X"), 204 | "utf8" 205 | ); 206 | 207 | original = original.replace(/\s/g, ""); 208 | generated = generated.replace(/\s/g, ""); 209 | 210 | expect(original === generated).toBeTruthy(); 211 | 212 | fs.unlinkSync(path.resolve(__dirname, "./__test-data__/generated_datatype.L5X")); 213 | }); 214 | 215 | test("_orderify: Results in the Correct Order", () => { 216 | docEmpty 217 | .findOne("Controller") 218 | .dom.elements.push({ type: "element", name: "Tags", elements: [] }); 219 | 220 | docEmpty.findOne("Controller").dom.elements.push({ 221 | type: "element", 222 | name: "Trends", 223 | elements: [] 224 | }); 225 | 226 | docEmpty.findOne("Controller").dom.elements.push({ 227 | type: "element", 228 | name: "DataTypes", 229 | elements: [] 230 | }); 231 | 232 | docEmpty.findOne("Controller").dom.elements.push({ 233 | type: "element", 234 | name: "Description", 235 | elements: [] 236 | }); 237 | 238 | docEmpty._orderify(); 239 | expect(docEmpty).toMatchSnapshot(); 240 | }); 241 | }); 242 | }); 243 | -------------------------------------------------------------------------------- /src/document/index.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | const Element = require("../element"); 3 | const Tag = require("../tag"); 4 | const Program = require("../program"); 5 | const { resolve } = require("path"); 6 | const { hash } = require("../utilities"); 7 | const { xml2js } = require("xml-js"); 8 | 9 | const DOCUMENT_ID = hash("DOCUMENT_ID_STRING"); 10 | 11 | class Document extends Element { 12 | constructor(filepath = null) { 13 | if (filepath && typeof filepath !== "string") 14 | throw new Error(`Document expected to receive filepath of type instead got <${typeof filepath}>`); 15 | 16 | // Call parent constructor 17 | if (filepath) { 18 | // Start from existing L5X file 19 | super(xml2js(fs.readFileSync(resolve(filepath)), "utf8")); 20 | } else { 21 | // No filepath or tree given, initialize new document 22 | super({ 23 | declaration: { 24 | attributes: { version: "1.0", encoding: "UTF-8", standalone: "yes" } 25 | }, 26 | elements: [ 27 | { 28 | type: "element", 29 | name: "RSLogix5000Content", 30 | attributes: { SchemaRevision: "1.0", ContainsContext: true }, 31 | elements: [ 32 | { 33 | type: "element", 34 | name: "Controller", 35 | attributes: { Use: "Context" }, 36 | elements: [] 37 | } 38 | ] 39 | } 40 | ] 41 | }); 42 | } 43 | 44 | this.class_id = DOCUMENT_ID; 45 | } 46 | 47 | /** 48 | * Adds tag to controller instance 49 | * 50 | * @param {Tag} tag 51 | * @memberof Program 52 | */ 53 | addTag(tag) { 54 | if (!Tag.isTag(tag)) throw new Error(`addTag expected tag of type instead got <${typeof tag}>`); 55 | 56 | const controller = this.findOne("Controller"); 57 | const tags = controller.findOne("Tags", null, ["Programs"]); 58 | 59 | /* eslint-disable indent */ 60 | if (tags) tags.append(tag); 61 | else 62 | controller.append( 63 | new Element({ 64 | type: "element", 65 | name: "Tags", 66 | elements: [] 67 | }).append(tag) 68 | ); 69 | /* eslint-enable indent */ 70 | 71 | this._orderify(); 72 | return this; 73 | } 74 | 75 | /** 76 | * Adds program to controller instance 77 | * 78 | * @param {Program} prog 79 | * @memberof Program 80 | */ 81 | addProgram(prog) { 82 | if (!Program.isProgram(prog)) 83 | throw new Error(`addProgram expected prog of type instead got <${typeof prog}>`); 84 | 85 | const controller = this.findOne("Controller"); 86 | const programs = controller.findOne("Programs"); 87 | 88 | /* eslint-disable indent */ 89 | if (programs) programs.append(prog); 90 | else 91 | controller.append( 92 | new Element({ 93 | type: "element", 94 | name: "Programs", 95 | elements: [] 96 | }).append(prog) 97 | ); 98 | /* eslint-enable indent */ 99 | 100 | this._orderify(); 101 | return this; 102 | } 103 | 104 | /** 105 | * Finds tag in the controller instance that matches the name 106 | * 107 | * @param {string} name 108 | * @returns {Tag|null} 109 | * @memberof Program 110 | */ 111 | findTag(name) { 112 | if (typeof name !== "string") 113 | throw new Error(`findTag expected name of type instead got <${typeof name}>`); 114 | 115 | const found = this.findOne("Tag", { Name: name }, ["Programs"]); 116 | if (!found) return null; 117 | 118 | const tag = new Tag(name, found.dom.attributes.DataType); 119 | tag._dom = found.dom; 120 | 121 | return tag; 122 | } 123 | 124 | /** 125 | * Finds program in the controller instance that matches the name 126 | * 127 | * @param {string} name 128 | * @returns {Program|null} 129 | * @memberof Program 130 | */ 131 | findProgram(name) { 132 | if (typeof name !== "string") 133 | throw new Error(`findProgram expected name of type instead got <${typeof name}>`); 134 | 135 | const found = this.findOne("Program", { Name: name }); 136 | if (!found) return null; 137 | 138 | const prog = new Program(name); 139 | prog._dom = found.dom; 140 | 141 | return prog; 142 | } 143 | 144 | /** 145 | * Sets passed element as document target element 146 | * --> Only if a valid element subtype 147 | * 148 | * @param {Element} target 149 | * @memberof Document 150 | */ 151 | setTarget(target) { 152 | if (!Element.isElement(target)) 153 | throw new Error(`setTarget expected to receive target of type instead got <${typeof target}>`); 154 | 155 | const prog = this.findOne("Program", { Use: "Target" }); 156 | if (prog) prog.dom.attributes.Use = "Context"; 157 | 158 | const rout = this.findOne("Routine", { Use: "Target" }); 159 | if (rout) rout.dom.attributes.Use = "Context"; 160 | 161 | this.dom.elements[0].attributes.TargetName = target.dom.attributes.Name; 162 | target.dom.attributes.Use = "Target"; 163 | 164 | let routines = null; 165 | 166 | /* eslint-disable indent */ 167 | switch (target.dom.name) { 168 | case "Program": 169 | this.dom.elements[0].attributes.TargetType = "Program"; 170 | routines = target.find("Routine"); 171 | 172 | if (routines) { 173 | for (const routine of routines) delete routine.dom.attributes.Use; 174 | } 175 | 176 | break; 177 | case "Routine": 178 | this.dom.elements[0].attributes.TargetType = "Routine"; 179 | break; 180 | default: 181 | throw new Error( 182 | `setTarget could not set passed element as Target - Unrecognized Element Type <${target.name}>` 183 | ); 184 | } 185 | /* eslint-enable indent */ 186 | } 187 | 188 | /** 189 | * Puts the programs children in the correct order 190 | * 191 | * @private 192 | * @memberof Program 193 | */ 194 | _orderify() { 195 | const order = { 196 | Description: 1, 197 | RedundancyInfo: 2, 198 | Security: 3, 199 | SafetyInfo: 4, 200 | DataTypes: 5, 201 | Modules: 6, 202 | AddOnInstructionDefinitions: 7, 203 | Tags: 8, 204 | Programs: 9, 205 | Tasks: 10, 206 | ParameterConnections: 11, 207 | CommPorts: 12, 208 | CST: 13, 209 | WallClockTime: 14, 210 | Trends: 15, 211 | QuickWatcherLists: 16, 212 | InternetProtocol: 17, 213 | EthernetPorts: 18, 214 | EthernetNetwork: 19 215 | }; 216 | 217 | this.findOne("Controller").dom.elements.sort((a, b) => { 218 | return order[a.name] - order[b.name]; 219 | }); 220 | } 221 | 222 | /** 223 | * Exports document to provided filepath 224 | * 225 | * @param {string} filepath 226 | * @memberof Document 227 | */ 228 | export(filepath) { 229 | fs.writeFileSync(resolve(filepath), this.toString(), "utf8"); 230 | } 231 | 232 | /** 233 | * Check if passed object is a document 234 | * 235 | * @static 236 | * @param {object} doc - object to test 237 | * @returns {boolean} 238 | * @memberof Document 239 | */ 240 | static isDocument(doc) { 241 | return doc.class_id && doc.class_id === DOCUMENT_ID; 242 | } 243 | } 244 | 245 | module.exports = Document; 246 | -------------------------------------------------------------------------------- /src/element/__snapshots__/element.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Element Class Instance Methods append: Appends New Element 1`] = ` 4 | Element { 5 | "_dom": Object { 6 | "elements": Array [ 7 | Object { 8 | "elements": Array [ 9 | Object { 10 | "elements": Array [ 11 | Object { 12 | "attributes": Object { 13 | "Name": "TagName", 14 | }, 15 | "elements": Array [], 16 | "name": "Tag", 17 | "type": "element", 18 | }, 19 | ], 20 | "name": "Tags", 21 | "type": "element", 22 | }, 23 | Object { 24 | "elements": Array [ 25 | Object { 26 | "elements": Array [ 27 | Object { 28 | "elements": Array [ 29 | Object { 30 | "attributes": Object { 31 | "Name": "AnotherTagName", 32 | }, 33 | "elements": Array [], 34 | "name": "Tag", 35 | "type": "element", 36 | }, 37 | ], 38 | "name": "Tags", 39 | "type": "element", 40 | }, 41 | ], 42 | "name": "Program", 43 | "type": "element", 44 | }, 45 | ], 46 | "name": "Programs", 47 | "type": "element", 48 | }, 49 | Object { 50 | "name": "Tags", 51 | "type": "element", 52 | }, 53 | ], 54 | "name": "Controller", 55 | "type": "element", 56 | }, 57 | ], 58 | "name": "root", 59 | "type": "element", 60 | }, 61 | "elem_id": "33cb4159377b63ce9db11667160e39df", 62 | } 63 | `; 64 | 65 | exports[`Element Class Instance Methods append: Appends New Element 2`] = ` 66 | Element { 67 | "_dom": Object { 68 | "elements": Array [ 69 | Object { 70 | "elements": Array [ 71 | Object { 72 | "elements": Array [ 73 | Object { 74 | "attributes": Object { 75 | "Name": "TagName", 76 | }, 77 | "elements": Array [], 78 | "name": "Tag", 79 | "type": "element", 80 | }, 81 | ], 82 | "name": "Tags", 83 | "type": "element", 84 | }, 85 | Object { 86 | "elements": Array [ 87 | Object { 88 | "elements": Array [ 89 | Object { 90 | "elements": Array [ 91 | Object { 92 | "attributes": Object { 93 | "Name": "AnotherTagName", 94 | }, 95 | "elements": Array [], 96 | "name": "Tag", 97 | "type": "element", 98 | }, 99 | ], 100 | "name": "Tags", 101 | "type": "element", 102 | }, 103 | ], 104 | "name": "Program", 105 | "type": "element", 106 | }, 107 | ], 108 | "name": "Programs", 109 | "type": "element", 110 | }, 111 | Object { 112 | "elements": Array [ 113 | Object { 114 | "attributes": Object { 115 | "Name": "SomeTag", 116 | "TagType": "Base", 117 | }, 118 | "name": "Tag", 119 | "type": "element", 120 | }, 121 | ], 122 | "name": "Tags", 123 | "type": "element", 124 | }, 125 | ], 126 | "name": "Controller", 127 | "type": "element", 128 | }, 129 | ], 130 | "name": "root", 131 | "type": "element", 132 | }, 133 | "elem_id": "33cb4159377b63ce9db11667160e39df", 134 | } 135 | `; 136 | 137 | exports[`Element Class Instance Methods find: Finds Document Elements and Returns New Document 1`] = ` 138 | Array [ 139 | Element { 140 | "_dom": Object { 141 | "elements": Array [ 142 | Object { 143 | "elements": Array [ 144 | Object { 145 | "attributes": Object { 146 | "Name": "TagName", 147 | }, 148 | "elements": Array [], 149 | "name": "Tag", 150 | "type": "element", 151 | }, 152 | ], 153 | "name": "Tags", 154 | "type": "element", 155 | }, 156 | Object { 157 | "elements": Array [ 158 | Object { 159 | "elements": Array [ 160 | Object { 161 | "elements": Array [ 162 | Object { 163 | "attributes": Object { 164 | "Name": "AnotherTagName", 165 | }, 166 | "elements": Array [], 167 | "name": "Tag", 168 | "type": "element", 169 | }, 170 | ], 171 | "name": "Tags", 172 | "type": "element", 173 | }, 174 | ], 175 | "name": "Program", 176 | "type": "element", 177 | }, 178 | ], 179 | "name": "Programs", 180 | "type": "element", 181 | }, 182 | ], 183 | "name": "Controller", 184 | "type": "element", 185 | }, 186 | "elem_id": "33cb4159377b63ce9db11667160e39df", 187 | }, 188 | ] 189 | `; 190 | 191 | exports[`Element Class Instance Methods find: Finds Document Elements and Returns New Document 2`] = ` 192 | Array [ 193 | Element { 194 | "_dom": Object { 195 | "attributes": Object { 196 | "Name": "TagName", 197 | }, 198 | "elements": Array [], 199 | "name": "Tag", 200 | "type": "element", 201 | }, 202 | "elem_id": "33cb4159377b63ce9db11667160e39df", 203 | }, 204 | ] 205 | `; 206 | 207 | exports[`Element Class Instance Methods find: Finds Document Elements and Returns New Document 3`] = ` 208 | Array [ 209 | Element { 210 | "_dom": Object { 211 | "attributes": Object { 212 | "Name": "TagName", 213 | }, 214 | "elements": Array [], 215 | "name": "Tag", 216 | "type": "element", 217 | }, 218 | "elem_id": "33cb4159377b63ce9db11667160e39df", 219 | }, 220 | Element { 221 | "_dom": Object { 222 | "attributes": Object { 223 | "Name": "AnotherTagName", 224 | }, 225 | "elements": Array [], 226 | "name": "Tag", 227 | "type": "element", 228 | }, 229 | "elem_id": "33cb4159377b63ce9db11667160e39df", 230 | }, 231 | ] 232 | `; 233 | 234 | exports[`Element Class Instance Methods find: Finds Document Elements and Returns New Document 4`] = ` 235 | Array [ 236 | Element { 237 | "_dom": Object { 238 | "attributes": Object { 239 | "Name": "TagName", 240 | }, 241 | "elements": Array [], 242 | "name": "Tag", 243 | "type": "element", 244 | }, 245 | "elem_id": "33cb4159377b63ce9db11667160e39df", 246 | }, 247 | ] 248 | `; 249 | 250 | exports[`Element Class Instance Methods find: Finds Document Elements and Returns New Document 5`] = ` 251 | Array [ 252 | Element { 253 | "_dom": Object { 254 | "attributes": Object { 255 | "Name": "AnotherTagName", 256 | }, 257 | "elements": Array [], 258 | "name": "Tag", 259 | "type": "element", 260 | }, 261 | "elem_id": "33cb4159377b63ce9db11667160e39df", 262 | }, 263 | ] 264 | `; 265 | 266 | exports[`Element Class Instance Methods findOne: Finds First Document Element 1`] = ` 267 | Element { 268 | "_dom": Object { 269 | "elements": Array [ 270 | Object { 271 | "elements": Array [ 272 | Object { 273 | "attributes": Object { 274 | "Name": "TagName", 275 | }, 276 | "elements": Array [], 277 | "name": "Tag", 278 | "type": "element", 279 | }, 280 | ], 281 | "name": "Tags", 282 | "type": "element", 283 | }, 284 | Object { 285 | "elements": Array [ 286 | Object { 287 | "elements": Array [ 288 | Object { 289 | "elements": Array [ 290 | Object { 291 | "attributes": Object { 292 | "Name": "AnotherTagName", 293 | }, 294 | "elements": Array [], 295 | "name": "Tag", 296 | "type": "element", 297 | }, 298 | ], 299 | "name": "Tags", 300 | "type": "element", 301 | }, 302 | ], 303 | "name": "Program", 304 | "type": "element", 305 | }, 306 | ], 307 | "name": "Programs", 308 | "type": "element", 309 | }, 310 | ], 311 | "name": "Controller", 312 | "type": "element", 313 | }, 314 | "elem_id": "33cb4159377b63ce9db11667160e39df", 315 | } 316 | `; 317 | 318 | exports[`Element Class Instance Methods findOne: Finds First Document Element 2`] = ` 319 | Element { 320 | "_dom": Object { 321 | "attributes": Object { 322 | "Name": "AnotherTagName", 323 | }, 324 | "elements": Array [], 325 | "name": "Tag", 326 | "type": "element", 327 | }, 328 | "elem_id": "33cb4159377b63ce9db11667160e39df", 329 | } 330 | `; 331 | 332 | exports[`Element Class Instance Methods findOne: Finds First Document Element 3`] = ` 333 | Element { 334 | "_dom": Object { 335 | "attributes": Object { 336 | "Name": "TagName", 337 | }, 338 | "elements": Array [], 339 | "name": "Tag", 340 | "type": "element", 341 | }, 342 | "elem_id": "33cb4159377b63ce9db11667160e39df", 343 | } 344 | `; 345 | 346 | exports[`Element Class Instance Methods findOne: Finds First Document Element 4`] = ` 347 | Element { 348 | "_dom": Object { 349 | "attributes": Object { 350 | "Name": "TagName", 351 | }, 352 | "elements": Array [], 353 | "name": "Tag", 354 | "type": "element", 355 | }, 356 | "elem_id": "33cb4159377b63ce9db11667160e39df", 357 | } 358 | `; 359 | 360 | exports[`Element Class Instance Methods get dom: Gets Dom Reference 1`] = ` 361 | Object { 362 | "elements": Array [ 363 | Object { 364 | "elements": Array [ 365 | Object { 366 | "elements": Array [ 367 | Object { 368 | "attributes": Object { 369 | "Name": "TagName", 370 | }, 371 | "elements": Array [], 372 | "name": "Tag", 373 | "type": "element", 374 | }, 375 | ], 376 | "name": "Tags", 377 | "type": "element", 378 | }, 379 | Object { 380 | "elements": Array [ 381 | Object { 382 | "elements": Array [ 383 | Object { 384 | "elements": Array [ 385 | Object { 386 | "attributes": Object { 387 | "Name": "AnotherTagName", 388 | }, 389 | "elements": Array [], 390 | "name": "Tag", 391 | "type": "element", 392 | }, 393 | ], 394 | "name": "Tags", 395 | "type": "element", 396 | }, 397 | ], 398 | "name": "Program", 399 | "type": "element", 400 | }, 401 | ], 402 | "name": "Programs", 403 | "type": "element", 404 | }, 405 | ], 406 | "name": "Controller", 407 | "type": "element", 408 | }, 409 | ], 410 | "name": "root", 411 | "type": "element", 412 | } 413 | `; 414 | 415 | exports[`Element Class Instance Methods toString: Generates Appropriate Output 1`] = `""`; 416 | 417 | exports[`Element Class New Instance Initializes with Desired Document Model 1`] = ` 418 | Element { 419 | "_dom": Object { 420 | "attr": "value", 421 | }, 422 | "elem_id": "33cb4159377b63ce9db11667160e39df", 423 | } 424 | `; 425 | -------------------------------------------------------------------------------- /src/element/element.spec.js: -------------------------------------------------------------------------------- 1 | const Element = require("./index"); 2 | 3 | describe("Element Class", () => { 4 | describe("New Instance", () => { 5 | it("Accepts Proper Input", () => { 6 | const fn = arg => () => new Element(arg); 7 | 8 | expect(fn("./__test-data__/datatype.L5X")).toThrow(); 9 | expect(fn(57)).toThrow(); 10 | expect( 11 | fn({ 12 | type: "element", 13 | name: "Element", 14 | elements: [] 15 | }) 16 | ).not.toThrow(); 17 | }); 18 | 19 | it("Initializes with Desired Document Model", () => { 20 | const doc = new Element({ 21 | attr: "value" 22 | }); 23 | 24 | expect(doc).toMatchSnapshot(); 25 | }); 26 | }); 27 | 28 | describe("Static Methods", () => { 29 | test("isDocument: Returns Appropriate Judgement", () => { 30 | const doc = new Element({ 31 | type: "element", 32 | name: "elem", 33 | elements: [] 34 | }); 35 | 36 | const fake = {}; 37 | 38 | expect(doc).toBeTruthy(); 39 | expect(Element.isElement(fake)).toBeFalsy(); 40 | }); 41 | }); 42 | 43 | describe("Instance Methods", () => { 44 | let doc; 45 | 46 | beforeEach(() => { 47 | doc = new Element({ 48 | type: "element", 49 | name: "root", 50 | elements: [ 51 | { 52 | type: "element", 53 | name: "Controller", 54 | elements: [ 55 | { 56 | type: "element", 57 | name: "Tags", 58 | elements: [ 59 | { 60 | type: "element", 61 | name: "Tag", 62 | attributes: { 63 | Name: "TagName" 64 | }, 65 | elements: [] 66 | } 67 | ] 68 | }, 69 | { 70 | type: "element", 71 | name: "Programs", 72 | elements: [ 73 | { 74 | type: "element", 75 | name: "Program", 76 | elements: [ 77 | { 78 | type: "element", 79 | name: "Tags", 80 | elements: [ 81 | { 82 | type: "element", 83 | name: "Tag", 84 | attributes: { 85 | Name: "AnotherTagName" 86 | }, 87 | elements: [] 88 | } 89 | ] 90 | } 91 | ] 92 | } 93 | ] 94 | } 95 | ] 96 | } 97 | ] 98 | }); 99 | }); 100 | 101 | test("get dom: Gets Dom Reference", () => { 102 | expect(doc.dom).toMatchSnapshot(); 103 | }); 104 | 105 | test("findOne: Finds First Document Element", () => { 106 | expect(doc.findOne("nothing")).toBeNull(); 107 | expect(doc.findOne("Controller")).not.toBeNull(); 108 | expect(doc.findOne("Controller")).toMatchSnapshot(); 109 | expect(doc.findOne("Member", { Name: "Push" })).toBeNull(); 110 | expect(doc.findOne("Tag", { Name: "AnotherTagName" })).toMatchSnapshot(); 111 | expect(doc.findOne("Tag", { Name: "AnotherTagName" }, ["Tags"])).toBeNull(); 112 | expect(doc.findOne("Tag")).toMatchSnapshot(); 113 | expect(doc.findOne("Controller").findOne("Tag")).toMatchSnapshot(); 114 | expect(doc.findOne("Controller").findOne("Member", { name: "Push" })).toBeNull(); 115 | }); 116 | 117 | test("findOne: Rejects Invalid Inputs", () => { 118 | const fn = (type, attr, ignore = [], tree = null) => () => 119 | doc.findOne(type, attr, ignore, tree); 120 | 121 | expect(fn(12, null, [], null)).toThrow(); 122 | expect(fn("Controller", null, [], null)).not.toThrow(); 123 | expect(fn("Controller", 12, null)).toThrow(); 124 | expect(fn("Tag", { Name: "TagName" }, [], null)).not.toThrow(); 125 | expect(fn("Tag", { Name: "TagName" }, [], "string")).toThrow(); 126 | expect(fn("Tag", { Name: "TagName" }, 12)).toThrow(); 127 | expect(fn("Tag", { Name: "TagName" }, [], {})).not.toThrow(); 128 | expect(fn("Tag", { Name: "TagName" }, "throw")).toThrow(); 129 | }); 130 | 131 | test("find: Finds Document Elements and Returns New Document", () => { 132 | expect(doc.find("nothing")).toBeNull(); 133 | 134 | expect(doc.find("Controller")).not.toBeNull(); 135 | expect(doc.find("Controller")).toMatchSnapshot(); 136 | 137 | expect(doc.find("Tag", { Name: "TagName" })).not.toBeNull(); 138 | expect(doc.find("Member")).toBeNull(); 139 | 140 | expect(doc.find("Tag", { Name: "TagName" })).toMatchSnapshot(); 141 | expect(doc.find("Tag")).toMatchSnapshot(); 142 | 143 | expect(doc.find("Tags")[0].find("Tag")).toMatchSnapshot(); 144 | expect(doc.find("Tags")[1].find("Tag")).toMatchSnapshot(); 145 | 146 | expect(doc.find("Tag", {}, ["Tags"])).toBeNull(); 147 | }); 148 | 149 | test("find: Rejects Invalid Inputs", () => { 150 | const fn = (type, attr, ignore = [], tree) => () => doc.find(type, attr, ignore, tree); 151 | 152 | expect(fn(12, null, null)).toThrow(); 153 | expect(fn("Controller", null)).not.toThrow(); 154 | expect(fn("Controller", 12, null)).toThrow(); 155 | expect(fn("Tag", { Name: "TagName" })).not.toThrow(); 156 | expect(fn("Tag", { Name: "TagName" }, 12)).toThrow(); 157 | expect(fn("Tag", { Name: "TagName" }, [], {})).not.toThrow(); 158 | expect(fn("Tag", { Name: "TagName" }, {}, "Program")).toThrow(); 159 | expect(fn("Member", { Name: "another" }, ["DataTypes"], {})).not.toThrow(); 160 | }); 161 | 162 | test("append: Throws on Invalid Inputs", () => { 163 | const fn = arg => () => doc.append(arg); 164 | 165 | expect(fn({ key: "value" })).toThrow(); 166 | expect(fn(doc)).not.toThrow(); 167 | }); 168 | 169 | test("append: Appends New Element", () => { 170 | const tags = new Element({ 171 | type: "element", 172 | name: "Tags" 173 | }); 174 | 175 | doc.findOne("Controller").append(tags); 176 | expect(doc).toMatchSnapshot(); 177 | 178 | tags.append( 179 | new Element({ 180 | type: "element", 181 | name: "Tag", 182 | attributes: { 183 | Name: "SomeTag", 184 | TagType: "Base" 185 | } 186 | }) 187 | ); 188 | expect(doc).toMatchSnapshot(); 189 | }); 190 | 191 | test("toString: Generates Appropriate Output", () => { 192 | expect(doc.toString()).toMatchSnapshot(); 193 | }); 194 | }); 195 | }); 196 | -------------------------------------------------------------------------------- /src/element/index.js: -------------------------------------------------------------------------------- 1 | const { hash } = require("../utilities"); 2 | const { js2xml } = require("xml-js"); 3 | 4 | const ELEMENT_ID = hash("ELEMENT_ID_STRING"); 5 | 6 | class Element { 7 | constructor(document_tree) { 8 | this.elem_id = ELEMENT_ID; 9 | 10 | if (typeof document_tree !== "object") 11 | throw new Error( 12 | `Element must be initialized with a document_tree of type instead got ${typeof document_tree}` 13 | ); 14 | 15 | // Initialize Base L5X Dom Object 16 | // --> This Object is in the form that the 17 | // --> package 'xml-js' will understand 18 | // No filepath or tree given, initialize new document 19 | this._dom = document_tree; 20 | } 21 | 22 | /** 23 | * Returns a mutable dom reference to be manipulated by user 24 | * 25 | * @readonly 26 | * @memberof Document 27 | */ 28 | get dom() { 29 | return this._dom; 30 | } 31 | 32 | /** 33 | * Finds the first desired element within the Document 34 | * 35 | * @param {string} type - Type of element to look for (e.g. Controller, Datatype, etc) 36 | * @param {object} [attr=null] - Object of attributes to match in element 37 | * @returns {object|null} found documents 38 | * @param {object} [search_tree=null] - Tree to search on 39 | * @memberof Document 40 | */ 41 | findOne(type, attr = null, ignore = [], search_tree = null) { 42 | // Validate Inputs 43 | if (typeof type !== "string") 44 | throw new Error( 45 | `Find expected a type name of type instead got type <${typeof type}>` 46 | ); 47 | 48 | if (attr && typeof attr !== "object") 49 | throw new Error( 50 | `Find expected a attr of type or null instead got <${typeof attr}>` 51 | ); 52 | 53 | if (search_tree && typeof search_tree !== "object") 54 | throw new Error( 55 | `Find expected a search tree of type or null instead got <${typeof search_tree}>` 56 | ); 57 | 58 | if (!Array.isArray(ignore)) 59 | throw new Error( 60 | `Find expected a ignore of type instead got <${typeof ignore}>` 61 | ); 62 | 63 | // If no tree is passed then search from document root 64 | let tree = search_tree ? search_tree : this._dom; 65 | 66 | // Nothing left to search, return null 67 | if (!tree.elements || !Array.isArray(tree.elements) || tree.length === 0) return null; 68 | 69 | // Search elements 70 | for (let elem of tree.elements) { 71 | if (!ignore.includes(elem.name)) { 72 | // Check if this is the desired element 73 | if (elem.name === type) { 74 | // Check if attributes match 75 | if (attr) { 76 | let attrMatch = true; 77 | for (let key of Object.keys(attr)) { 78 | if (!elem.attributes[key] || elem.attributes[key] !== attr[key]) 79 | attrMatch = false; 80 | } 81 | 82 | // Attributes match so return subtree 83 | if (attrMatch) return new Element(elem); 84 | } else { 85 | // No attributes need to match 86 | return new Element(elem); 87 | } 88 | } 89 | // Search branches for desired element 90 | const found = this.findOne(type, attr, ignore, elem); 91 | 92 | if (found) return found; 93 | } 94 | } 95 | 96 | // Nothing found, return null 97 | return null; 98 | } 99 | 100 | /** 101 | * Finds desired element within the Document 102 | * 103 | * @param {string} type - Type of element to look for (e.g. Controller, Datatype, etc) 104 | * @param {object} [attr=null] - Object of attributes to match in element 105 | * @param {Array} [ignore=null] - Names to Ignore 106 | * @param {object} [search_tree=null] - Tree to search on 107 | * @returns {Array|null} found documents 108 | * @memberof Document 109 | */ 110 | find(type, attr = null, ignore = [], search_tree = null) { 111 | // Validate Inputs 112 | if (typeof type !== "string") 113 | throw new Error( 114 | `Find expected a type name of type or null instead got type <${typeof type}>` 115 | ); 116 | 117 | if (attr && typeof attr !== "object") 118 | throw new Error( 119 | `Find expected a attr of type or null instead got <${typeof attr}>` 120 | ); 121 | 122 | if (search_tree && typeof search_tree !== "object") 123 | throw new Error( 124 | `Find expected a search tree of type or null instead got <${typeof search_tree}>` 125 | ); 126 | 127 | if (!Array.isArray(ignore)) 128 | throw new Error( 129 | `Find expected a ignore of type instead got <${typeof ignore}>` 130 | ); 131 | 132 | let returnArray = []; 133 | 134 | // Search function definiton 135 | const _find = (type, attr, search_tree) => { 136 | // Nothing left to search, return null 137 | if ( 138 | !search_tree.elements || 139 | !Array.isArray(search_tree.elements) || 140 | search_tree.elements.length === 0 141 | ) 142 | return null; 143 | 144 | // Search elements 145 | for (let elem of search_tree.elements) { 146 | // explore tree if not in ignores 147 | if (!ignore.includes(elem.name)) { 148 | // Check if this is the desired element 149 | if (elem.name === type) { 150 | // Check if attributes match 151 | if (attr) { 152 | let attrMatch = true; 153 | for (let key of Object.keys(attr)) { 154 | if (!elem.attributes[key] || elem.attributes[key] !== attr[key]) 155 | attrMatch = false; 156 | } 157 | 158 | // Attributes match so return subtree 159 | if (attrMatch) { 160 | returnArray.push(new Element(elem)); 161 | } 162 | } else { 163 | // No attributes need to match 164 | returnArray.push(new Element(elem)); 165 | } 166 | } else { 167 | _find(type, attr, elem); 168 | } 169 | } 170 | } 171 | }; 172 | 173 | // If no tree is passed then search from document root 174 | let tree = search_tree ? search_tree : this._dom; 175 | 176 | _find(type, attr, tree); 177 | 178 | return returnArray.length > 0 ? returnArray : null; 179 | } 180 | 181 | /** 182 | * Appends element to elem._dom.elements 183 | * 184 | * @param {Document} elem 185 | * @returns {Document} - this (for chaining) 186 | * @memberof Document 187 | */ 188 | append(elem) { 189 | if (!Element.isElement(elem)) 190 | throw new Error( 191 | `Append expected to receive doc of type instead got type <${typeof doc}>` 192 | ); 193 | 194 | if (this.dom.elements && Array.isArray(this.dom.elements)) 195 | this.dom.elements.push(elem.dom); 196 | else this.dom.elements = [elem.dom]; 197 | 198 | return this; 199 | } 200 | 201 | /** 202 | * Returns a string representation of the current document 203 | * 204 | * @returns {string} 205 | * @memberof Document 206 | */ 207 | toString() { 208 | return js2xml(this._dom); 209 | } 210 | 211 | /** 212 | * Check if passed object is a document 213 | * 214 | * @static 215 | * @param {object} doc - object to test 216 | * @returns {boolean} 217 | * @memberof Document 218 | */ 219 | static isElement(doc) { 220 | return doc.elem_id && doc.elem_id === ELEMENT_ID; 221 | } 222 | } 223 | 224 | module.exports = Element; 225 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | const Document = require("./document"); 2 | const Tag = require("./tag"); 3 | const Program = require("./program"); 4 | const Routine = require("./routine"); 5 | const Rung = require("./rung"); 6 | const Util = require("./utilities"); 7 | 8 | module.exports = { Document, Tag, Program, Routine, Rung, Util }; -------------------------------------------------------------------------------- /src/member/__snapshots__/member.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Member Class New Instance Initializes with Desired Document Model 1`] = ` 4 | Member { 5 | "_dom": Object { 6 | "attributes": Object { 7 | "DataType": "DINT", 8 | "Hidden": false, 9 | "Name": "tagName", 10 | "Radix": "decimal", 11 | }, 12 | "elements": Array [], 13 | "name": "Member", 14 | "type": "element", 15 | }, 16 | "class_id": "cdb6dcc14d327d3c45e6e77e6648d07e", 17 | "elem_id": "33cb4159377b63ce9db11667160e39df", 18 | } 19 | `; 20 | 21 | exports[`Member Class New Instance Initializes with Desired Document Model 2`] = ` 22 | Member { 23 | "_dom": Object { 24 | "attributes": Object { 25 | "BitNumber": 0, 26 | "DataType": "BIT", 27 | "Hidden": false, 28 | "Name": "tagName", 29 | "Radix": "binary", 30 | "Target": "aTarget", 31 | }, 32 | "elements": Array [], 33 | "name": "Member", 34 | "type": "element", 35 | }, 36 | "class_id": "cdb6dcc14d327d3c45e6e77e6648d07e", 37 | "elem_id": "33cb4159377b63ce9db11667160e39df", 38 | } 39 | `; 40 | 41 | exports[`Member Class New Instance Initializes with Desired Document Model 3`] = ` 42 | Member { 43 | "_dom": Object { 44 | "attributes": Object { 45 | "BitNumber": 0, 46 | "DataType": "BIT", 47 | "Hidden": false, 48 | "Name": "tagName", 49 | "Radix": "binary", 50 | "Target": "aTarget", 51 | }, 52 | "elements": Array [], 53 | "name": "Member", 54 | "type": "element", 55 | }, 56 | "class_id": "cdb6dcc14d327d3c45e6e77e6648d07e", 57 | "elem_id": "33cb4159377b63ce9db11667160e39df", 58 | } 59 | `; 60 | 61 | exports[`Member Class New Instance Initializes with Desired Document Model 4`] = ` 62 | Member { 63 | "_dom": Object { 64 | "attributes": Object { 65 | "DataType": "DINT", 66 | "Hidden": false, 67 | "Name": "tagName", 68 | "Radix": "decimal", 69 | }, 70 | "elements": Array [ 71 | Object { 72 | "elements": Array [ 73 | Object { 74 | "cdata": "A Description", 75 | "type": "cdata", 76 | }, 77 | ], 78 | "name": "Description", 79 | "type": "element", 80 | }, 81 | ], 82 | "name": "Member", 83 | "type": "element", 84 | }, 85 | "class_id": "cdb6dcc14d327d3c45e6e77e6648d07e", 86 | "elem_id": "33cb4159377b63ce9db11667160e39df", 87 | } 88 | `; 89 | 90 | exports[`Member Class New Instance Initializes with Desired Document Model 5`] = ` 91 | Member { 92 | "_dom": Object { 93 | "attributes": Object { 94 | "DataType": "DINT", 95 | "Hidden": true, 96 | "Name": "tagName", 97 | "Radix": "decimal", 98 | }, 99 | "elements": Array [ 100 | Object { 101 | "elements": Array [ 102 | Object { 103 | "cdata": "A Description", 104 | "type": "cdata", 105 | }, 106 | ], 107 | "name": "Description", 108 | "type": "element", 109 | }, 110 | ], 111 | "name": "Member", 112 | "type": "element", 113 | }, 114 | "class_id": "cdb6dcc14d327d3c45e6e77e6648d07e", 115 | "elem_id": "33cb4159377b63ce9db11667160e39df", 116 | } 117 | `; 118 | 119 | exports[`Member Class New Instance Initializes with Desired Document Model 6`] = ` 120 | Member { 121 | "_dom": Object { 122 | "attributes": Object { 123 | "DataType": "DINT", 124 | "Hidden": false, 125 | "Name": "tagName", 126 | "Radix": "decimal", 127 | }, 128 | "elements": Array [ 129 | Object { 130 | "elements": Array [ 131 | Object { 132 | "cdata": "A Description", 133 | "type": "cdata", 134 | }, 135 | ], 136 | "name": "Description", 137 | "type": "element", 138 | }, 139 | ], 140 | "name": "Member", 141 | "type": "element", 142 | }, 143 | "class_id": "cdb6dcc14d327d3c45e6e77e6648d07e", 144 | "elem_id": "33cb4159377b63ce9db11667160e39df", 145 | } 146 | `; 147 | 148 | exports[`Member Class New Instance Initializes with Desired Document Model 7`] = ` 149 | Member { 150 | "_dom": Object { 151 | "attributes": Object { 152 | "DataType": "DINT", 153 | "Hidden": false, 154 | "Name": "tagName", 155 | "Radix": "decimal", 156 | }, 157 | "elements": Array [ 158 | Object { 159 | "elements": Array [ 160 | Object { 161 | "cdata": "A Description", 162 | "type": "cdata", 163 | }, 164 | ], 165 | "name": "Description", 166 | "type": "element", 167 | }, 168 | ], 169 | "name": "Member", 170 | "type": "element", 171 | }, 172 | "class_id": "cdb6dcc14d327d3c45e6e77e6648d07e", 173 | "elem_id": "33cb4159377b63ce9db11667160e39df", 174 | } 175 | `; 176 | -------------------------------------------------------------------------------- /src/member/index.js: -------------------------------------------------------------------------------- 1 | const Element = require("../element"); 2 | const { hash } = require("../utilities"); 3 | 4 | const MEMBER_ID = hash("MEMBER_ID_STRING"); 5 | 6 | class Member extends Element { 7 | constructor( 8 | name, 9 | datatype, 10 | description = null, 11 | hidden = false, 12 | target = null, 13 | bit_number = null 14 | ) { 15 | if (typeof name !== "string") 16 | throw new Error( 17 | `Member constructor expected name of type instead got <${typeof name}>` 18 | ); 19 | 20 | if (typeof datatype !== "string") 21 | throw new Error( 22 | `Member constructor expected datatype of type instead got <${typeof datatype}>` 23 | ); 24 | 25 | if (description && typeof description !== "string") 26 | throw new Error( 27 | `Member constructor expected description of type instead got <${typeof description}>` 28 | ); 29 | 30 | if (hidden && typeof hidden !== "boolean") 31 | throw new Error( 32 | `Member constructor expected hidden of type instead got <${typeof hidden}>` 33 | ); 34 | 35 | if (target && typeof target !== "string") 36 | throw new Error( 37 | `Member constructor expected target of type instead got <${typeof target}>` 38 | ); 39 | 40 | if (bit_number && !target) 41 | throw new Error("bit_number can only be defined with valid target name"); 42 | 43 | if (bit_number && typeof bit_number !== "number") 44 | throw new Error( 45 | `Member constructor expected bit_number of type instead got <${typeof bit_number}>` 46 | ); 47 | 48 | let attributes = { 49 | Name: name, 50 | DataType: datatype === "BOOL" ? "BIT" : datatype, 51 | Hidden: hidden, 52 | Radix: datatype === "BOOL" || datatype === "BIT" ? "binary" : "decimal" 53 | }; 54 | 55 | if ((datatype === "BOOL" || datatype === "BIT") && target) { 56 | attributes.Target = target; 57 | attributes.BitNumber = bit_number; 58 | } 59 | 60 | let elements = []; 61 | 62 | /* eslint-disable indent */ 63 | if (description) 64 | elements.push({ 65 | type: "element", 66 | name: "Description", 67 | elements: [ 68 | { 69 | type: "cdata", 70 | cdata: description 71 | } 72 | ] 73 | }); 74 | /* eslint-enable indent */ 75 | 76 | // Call parent class constructor 77 | super({ 78 | type: "element", 79 | name: "Member", 80 | attributes, 81 | elements 82 | }); 83 | 84 | this.class_id = MEMBER_ID; 85 | } 86 | 87 | /** 88 | * Checks if an incoming object is a member instance 89 | * 90 | * @static 91 | * @param {Member} member 92 | * @returns {boolean} 93 | * @memberof Tag 94 | */ 95 | static isMember(member) { 96 | return member.class_id && member.class_id === MEMBER_ID; 97 | } 98 | } 99 | 100 | module.exports = Member; 101 | -------------------------------------------------------------------------------- /src/member/member.spec.js: -------------------------------------------------------------------------------- 1 | const Member = require("./index"); 2 | 3 | describe("Member Class", () => { 4 | describe("New Instance", () => { 5 | it("Accepts Proper Input", () => { 6 | const fn = ( 7 | name, 8 | datatype, 9 | desc = null, 10 | hidden = false, 11 | target = null, 12 | bit = null 13 | ) => () => new Member(name, datatype, desc, hidden, target, bit); 14 | 15 | expect(fn("tagName")).toThrow(); 16 | expect(fn(null, "DINT")).toThrow(); 17 | expect(fn("tagName", "DINT")).not.toThrow(); 18 | expect(fn("tagName", "DINT", 12)).toThrow(); 19 | expect(fn("tagName", "DINT", "A Description")).not.toThrow(); 20 | expect(fn("tagName", "DINT", "A Description", 12)).toThrow(); 21 | expect(fn("tagName", "DINT", "A Description", true)).not.toThrow(); 22 | expect(fn("tagName", "DINT", "A Description", true, 12)).toThrow(); 23 | expect(fn("tagName", "DINT", "A Description", true, "aTarget")).not.toThrow(); 24 | expect( 25 | fn("tagName", "DINT", "A Description", true, "aTarget", "notANumber") 26 | ).toThrow(); 27 | expect(fn("tagName", "DINT", "A Description", true, null, 12)).toThrow(); 28 | expect( 29 | fn("tagName", "DINT", "A Description", true, "aTarget", 12) 30 | ).not.toThrow(); 31 | }); 32 | 33 | it("Initializes with Desired Document Model", () => { 34 | let mem = new Member("tagName", "DINT"); 35 | expect(mem).toMatchSnapshot(); 36 | 37 | mem = new Member("tagName", "BOOL", null, false, "aTarget", 0); 38 | expect(mem).toMatchSnapshot(); 39 | 40 | mem = new Member("tagName", "BIT", null, false, "aTarget", 0); 41 | expect(mem).toMatchSnapshot(); 42 | 43 | mem = new Member("tagName", "DINT", "A Description"); 44 | expect(mem).toMatchSnapshot(); 45 | 46 | mem = new Member("tagName", "DINT", "A Description", true); 47 | expect(mem).toMatchSnapshot(); 48 | 49 | mem = new Member("tagName", "DINT", "A Description", false, "ATarget"); 50 | expect(mem).toMatchSnapshot(); 51 | 52 | mem = new Member("tagName", "DINT", "A Description", false, "ATarget", 12); 53 | expect(mem).toMatchSnapshot(); 54 | }); 55 | }); 56 | 57 | describe("Static Methods", () => { 58 | test("isMember: Returns Appropriate Judgement", () => { 59 | let mem = new Member("tagName", "DINT"); 60 | expect(Member.isMember(mem)).toBeTruthy(); 61 | 62 | mem = { notATag: 12 }; 63 | expect(Member.isMember(mem)).toBeFalsy(); 64 | 65 | mem = 12; 66 | expect(Member.isMember(mem)).toBeFalsy(); 67 | }); 68 | }); 69 | }); 70 | -------------------------------------------------------------------------------- /src/program/__snapshots__/program.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Program Class Methods _orderify: Results in the Correct Order 1`] = ` 4 | Program { 5 | "_dom": Object { 6 | "attributes": Object { 7 | "Class": "Standard", 8 | "Name": "testProg", 9 | "Type": "Normal", 10 | "Use": "Context", 11 | }, 12 | "elements": Array [ 13 | Object { 14 | "elements": Array [], 15 | "name": "Description", 16 | "type": "element", 17 | }, 18 | Object { 19 | "elements": Array [], 20 | "name": "Tags", 21 | "type": "element", 22 | }, 23 | Object { 24 | "elements": Array [], 25 | "name": "Routines", 26 | "type": "element", 27 | }, 28 | ], 29 | "name": "Program", 30 | "type": "element", 31 | }, 32 | "class_id": "bacac87817ce803505fcdfaf4360dc50", 33 | "elem_id": "33cb4159377b63ce9db11667160e39df", 34 | } 35 | `; 36 | 37 | exports[`Program Class Methods addRoutine: Adds Routine Program Instance 1`] = ` 38 | Program { 39 | "_dom": Object { 40 | "attributes": Object { 41 | "Class": "Standard", 42 | "Name": "testProg", 43 | "Type": "Normal", 44 | "Use": "Context", 45 | }, 46 | "elements": Array [ 47 | Object { 48 | "elements": Array [ 49 | Object { 50 | "attributes": Object { 51 | "Name": "aRoutine", 52 | "Type": "RLL", 53 | "Use": "Context", 54 | }, 55 | "elements": Array [ 56 | Object { 57 | "elements": Array [], 58 | "name": "RLLContent", 59 | "type": "element", 60 | }, 61 | ], 62 | "name": "Routine", 63 | "type": "element", 64 | }, 65 | ], 66 | "name": "Routines", 67 | "type": "element", 68 | }, 69 | ], 70 | "name": "Program", 71 | "type": "element", 72 | }, 73 | "class_id": "bacac87817ce803505fcdfaf4360dc50", 74 | "elem_id": "33cb4159377b63ce9db11667160e39df", 75 | } 76 | `; 77 | 78 | exports[`Program Class Methods addRoutine: Adds Routine Program Instance 2`] = ` 79 | Program { 80 | "_dom": Object { 81 | "attributes": Object { 82 | "Class": "Standard", 83 | "Name": "testProg", 84 | "Type": "Normal", 85 | "Use": "Context", 86 | }, 87 | "elements": Array [ 88 | Object { 89 | "elements": Array [ 90 | Object { 91 | "attributes": Object { 92 | "Name": "aRoutine", 93 | "Type": "RLL", 94 | "Use": "Context", 95 | }, 96 | "elements": Array [ 97 | Object { 98 | "elements": Array [], 99 | "name": "RLLContent", 100 | "type": "element", 101 | }, 102 | ], 103 | "name": "Routine", 104 | "type": "element", 105 | }, 106 | Object { 107 | "attributes": Object { 108 | "Name": "bRoutine", 109 | "Type": "RLL", 110 | "Use": "Context", 111 | }, 112 | "elements": Array [ 113 | Object { 114 | "elements": Array [], 115 | "name": "RLLContent", 116 | "type": "element", 117 | }, 118 | ], 119 | "name": "Routine", 120 | "type": "element", 121 | }, 122 | ], 123 | "name": "Routines", 124 | "type": "element", 125 | }, 126 | ], 127 | "name": "Program", 128 | "type": "element", 129 | }, 130 | "class_id": "bacac87817ce803505fcdfaf4360dc50", 131 | "elem_id": "33cb4159377b63ce9db11667160e39df", 132 | } 133 | `; 134 | 135 | exports[`Program Class Methods addRoutine: Adds Routine Program Instance 3`] = ` 136 | Program { 137 | "_dom": Object { 138 | "attributes": Object { 139 | "Class": "Standard", 140 | "Name": "testProg", 141 | "Type": "Normal", 142 | "Use": "Context", 143 | }, 144 | "elements": Array [ 145 | Object { 146 | "elements": Array [ 147 | Object { 148 | "attributes": Object { 149 | "Name": "aRoutine", 150 | "Type": "RLL", 151 | "Use": "Context", 152 | }, 153 | "elements": Array [ 154 | Object { 155 | "elements": Array [], 156 | "name": "RLLContent", 157 | "type": "element", 158 | }, 159 | ], 160 | "name": "Routine", 161 | "type": "element", 162 | }, 163 | Object { 164 | "attributes": Object { 165 | "Name": "bRoutine", 166 | "Type": "RLL", 167 | "Use": "Context", 168 | }, 169 | "elements": Array [ 170 | Object { 171 | "elements": Array [], 172 | "name": "RLLContent", 173 | "type": "element", 174 | }, 175 | ], 176 | "name": "Routine", 177 | "type": "element", 178 | }, 179 | Object { 180 | "attributes": Object { 181 | "Name": "cRoutine", 182 | "Type": "RLL", 183 | "Use": "Context", 184 | }, 185 | "elements": Array [ 186 | Object { 187 | "elements": Array [], 188 | "name": "RLLContent", 189 | "type": "element", 190 | }, 191 | ], 192 | "name": "Routine", 193 | "type": "element", 194 | }, 195 | ], 196 | "name": "Routines", 197 | "type": "element", 198 | }, 199 | ], 200 | "name": "Program", 201 | "type": "element", 202 | }, 203 | "class_id": "bacac87817ce803505fcdfaf4360dc50", 204 | "elem_id": "33cb4159377b63ce9db11667160e39df", 205 | } 206 | `; 207 | 208 | exports[`Program Class Methods addRoutine: Adds Routine Program Instance 4`] = ` 209 | Program { 210 | "_dom": Object { 211 | "attributes": Object { 212 | "Class": "Standard", 213 | "Name": "testProg", 214 | "Type": "Normal", 215 | "Use": "Context", 216 | }, 217 | "elements": Array [ 218 | Object { 219 | "elements": Array [ 220 | Object { 221 | "attributes": Object { 222 | "Name": "aRoutine", 223 | "Type": "RLL", 224 | "Use": "Context", 225 | }, 226 | "elements": Array [ 227 | Object { 228 | "elements": Array [], 229 | "name": "RLLContent", 230 | "type": "element", 231 | }, 232 | ], 233 | "name": "Routine", 234 | "type": "element", 235 | }, 236 | Object { 237 | "attributes": Object { 238 | "Name": "bRoutine", 239 | "Type": "RLL", 240 | "Use": "Context", 241 | }, 242 | "elements": Array [ 243 | Object { 244 | "elements": Array [], 245 | "name": "RLLContent", 246 | "type": "element", 247 | }, 248 | ], 249 | "name": "Routine", 250 | "type": "element", 251 | }, 252 | Object { 253 | "attributes": Object { 254 | "Name": "cRoutine", 255 | "Type": "RLL", 256 | "Use": "Context", 257 | }, 258 | "elements": Array [ 259 | Object { 260 | "elements": Array [ 261 | Object { 262 | "attributes": Object { 263 | "Number": 0, 264 | "Type": "N", 265 | }, 266 | "elements": Array [ 267 | Object { 268 | "elements": Array [ 269 | Object { 270 | "cdata": "XIC(someTag)NOP();", 271 | "type": "cdata", 272 | }, 273 | ], 274 | "name": "Text", 275 | "type": "element", 276 | }, 277 | ], 278 | "name": "Rung", 279 | "type": "element", 280 | }, 281 | ], 282 | "name": "RLLContent", 283 | "type": "element", 284 | }, 285 | ], 286 | "name": "Routine", 287 | "type": "element", 288 | }, 289 | ], 290 | "name": "Routines", 291 | "type": "element", 292 | }, 293 | ], 294 | "name": "Program", 295 | "type": "element", 296 | }, 297 | "class_id": "bacac87817ce803505fcdfaf4360dc50", 298 | "elem_id": "33cb4159377b63ce9db11667160e39df", 299 | } 300 | `; 301 | 302 | exports[`Program Class Methods addTag: Adds Tag Program Instance 1`] = ` 303 | Program { 304 | "_dom": Object { 305 | "attributes": Object { 306 | "Class": "Standard", 307 | "Name": "testProg", 308 | "Type": "Normal", 309 | "Use": "Context", 310 | }, 311 | "elements": Array [ 312 | Object { 313 | "elements": Array [ 314 | Object { 315 | "attributes": Object { 316 | "Class": "Standard", 317 | "DataType": "BOOL", 318 | "Name": "tag1", 319 | "TagType": "Base", 320 | }, 321 | "elements": Array [], 322 | "name": "Tag", 323 | "type": "element", 324 | }, 325 | ], 326 | "name": "Tags", 327 | "type": "element", 328 | }, 329 | ], 330 | "name": "Program", 331 | "type": "element", 332 | }, 333 | "class_id": "bacac87817ce803505fcdfaf4360dc50", 334 | "elem_id": "33cb4159377b63ce9db11667160e39df", 335 | } 336 | `; 337 | 338 | exports[`Program Class Methods addTag: Adds Tag Program Instance 2`] = ` 339 | Program { 340 | "_dom": Object { 341 | "attributes": Object { 342 | "Class": "Standard", 343 | "Name": "testProg", 344 | "Type": "Normal", 345 | "Use": "Context", 346 | }, 347 | "elements": Array [ 348 | Object { 349 | "elements": Array [ 350 | Object { 351 | "attributes": Object { 352 | "Class": "Standard", 353 | "DataType": "BOOL", 354 | "Name": "tag1", 355 | "TagType": "Base", 356 | }, 357 | "elements": Array [], 358 | "name": "Tag", 359 | "type": "element", 360 | }, 361 | Object { 362 | "attributes": Object { 363 | "Class": "Standard", 364 | "DataType": "DINT", 365 | "Name": "tag2", 366 | "TagType": "Base", 367 | }, 368 | "elements": Array [], 369 | "name": "Tag", 370 | "type": "element", 371 | }, 372 | ], 373 | "name": "Tags", 374 | "type": "element", 375 | }, 376 | ], 377 | "name": "Program", 378 | "type": "element", 379 | }, 380 | "class_id": "bacac87817ce803505fcdfaf4360dc50", 381 | "elem_id": "33cb4159377b63ce9db11667160e39df", 382 | } 383 | `; 384 | 385 | exports[`Program Class Methods addTag: Adds Tag Program Instance 3`] = ` 386 | Program { 387 | "_dom": Object { 388 | "attributes": Object { 389 | "Class": "Standard", 390 | "Name": "testProg", 391 | "Type": "Normal", 392 | "Use": "Context", 393 | }, 394 | "elements": Array [ 395 | Object { 396 | "elements": Array [ 397 | Object { 398 | "cdata": "A Cool Program Description", 399 | "type": "cdata", 400 | }, 401 | ], 402 | "name": "Description", 403 | "type": "element", 404 | }, 405 | Object { 406 | "elements": Array [ 407 | Object { 408 | "attributes": Object { 409 | "Class": "Standard", 410 | "DataType": "BOOL", 411 | "Name": "tag1", 412 | "TagType": "Base", 413 | }, 414 | "elements": Array [], 415 | "name": "Tag", 416 | "type": "element", 417 | }, 418 | Object { 419 | "attributes": Object { 420 | "Class": "Standard", 421 | "DataType": "DINT", 422 | "Name": "tag2", 423 | "TagType": "Base", 424 | }, 425 | "elements": Array [], 426 | "name": "Tag", 427 | "type": "element", 428 | }, 429 | Object { 430 | "attributes": Object { 431 | "Class": "Standard", 432 | "DataType": "SINT", 433 | "Name": "tag3", 434 | "TagType": "Base", 435 | }, 436 | "elements": Array [], 437 | "name": "Tag", 438 | "type": "element", 439 | }, 440 | ], 441 | "name": "Tags", 442 | "type": "element", 443 | }, 444 | ], 445 | "name": "Program", 446 | "type": "element", 447 | }, 448 | "class_id": "bacac87817ce803505fcdfaf4360dc50", 449 | "elem_id": "33cb4159377b63ce9db11667160e39df", 450 | } 451 | `; 452 | 453 | exports[`Program Class Methods addTag: Adds Tag Program Instance 4`] = ` 454 | Program { 455 | "_dom": Object { 456 | "attributes": Object { 457 | "Class": "Standard", 458 | "Name": "testProg", 459 | "Type": "Normal", 460 | "Use": "Context", 461 | }, 462 | "elements": Array [ 463 | Object { 464 | "elements": Array [ 465 | Object { 466 | "cdata": "A Cool Program Description", 467 | "type": "cdata", 468 | }, 469 | ], 470 | "name": "Description", 471 | "type": "element", 472 | }, 473 | Object { 474 | "elements": Array [ 475 | Object { 476 | "attributes": Object { 477 | "Class": "Standard", 478 | "DataType": "BOOL", 479 | "Name": "tag1", 480 | "TagType": "Base", 481 | }, 482 | "elements": Array [], 483 | "name": "Tag", 484 | "type": "element", 485 | }, 486 | Object { 487 | "attributes": Object { 488 | "Class": "Standard", 489 | "DataType": "DINT", 490 | "Name": "tag2", 491 | "TagType": "Base", 492 | }, 493 | "elements": Array [], 494 | "name": "Tag", 495 | "type": "element", 496 | }, 497 | Object { 498 | "attributes": Object { 499 | "Class": "Standard", 500 | "DataType": "SINT", 501 | "Name": "tag3", 502 | "TagType": "Base", 503 | }, 504 | "elements": Array [], 505 | "name": "Tag", 506 | "type": "element", 507 | }, 508 | Object { 509 | "attributes": Object { 510 | "Class": "Standard", 511 | "DataType": "REAL", 512 | "Name": "tag4", 513 | "TagType": "Base", 514 | }, 515 | "elements": Array [], 516 | "name": "Tag", 517 | "type": "element", 518 | }, 519 | ], 520 | "name": "Tags", 521 | "type": "element", 522 | }, 523 | ], 524 | "name": "Program", 525 | "type": "element", 526 | }, 527 | "class_id": "bacac87817ce803505fcdfaf4360dc50", 528 | "elem_id": "33cb4159377b63ce9db11667160e39df", 529 | } 530 | `; 531 | 532 | exports[`Program Class Methods addTag: Adds Tag Program Instance 5`] = ` 533 | Program { 534 | "_dom": Object { 535 | "attributes": Object { 536 | "Class": "Standard", 537 | "Name": "testProg", 538 | "Type": "Normal", 539 | "Use": "Context", 540 | }, 541 | "elements": Array [ 542 | Object { 543 | "elements": Array [ 544 | Object { 545 | "cdata": "A Cool Program Description", 546 | "type": "cdata", 547 | }, 548 | ], 549 | "name": "Description", 550 | "type": "element", 551 | }, 552 | Object { 553 | "elements": Array [ 554 | Object { 555 | "attributes": Object { 556 | "Class": "Standard", 557 | "DataType": "BOOL", 558 | "Name": "tag1", 559 | "TagType": "Base", 560 | }, 561 | "elements": Array [], 562 | "name": "Tag", 563 | "type": "element", 564 | }, 565 | Object { 566 | "attributes": Object { 567 | "Class": "Standard", 568 | "DataType": "DINT", 569 | "Name": "tag2", 570 | "TagType": "Base", 571 | }, 572 | "elements": Array [], 573 | "name": "Tag", 574 | "type": "element", 575 | }, 576 | Object { 577 | "attributes": Object { 578 | "Class": "Standard", 579 | "DataType": "SINT", 580 | "Name": "tag3", 581 | "TagType": "Base", 582 | }, 583 | "elements": Array [], 584 | "name": "Tag", 585 | "type": "element", 586 | }, 587 | Object { 588 | "attributes": Object { 589 | "Class": "Standard", 590 | "DataType": "REAL", 591 | "Name": "not_tag4Anymore", 592 | "TagType": "Base", 593 | }, 594 | "elements": Array [], 595 | "name": "Tag", 596 | "type": "element", 597 | }, 598 | ], 599 | "name": "Tags", 600 | "type": "element", 601 | }, 602 | ], 603 | "name": "Program", 604 | "type": "element", 605 | }, 606 | "class_id": "bacac87817ce803505fcdfaf4360dc50", 607 | "elem_id": "33cb4159377b63ce9db11667160e39df", 608 | } 609 | `; 610 | 611 | exports[`Program Class Methods findRoutine: Finds Target Routine 1`] = ` 612 | Routine { 613 | "_dom": Object { 614 | "attributes": Object { 615 | "Name": "aRoutine", 616 | "Type": "RLL", 617 | "Use": "Context", 618 | }, 619 | "elements": Array [ 620 | Object { 621 | "elements": Array [], 622 | "name": "RLLContent", 623 | "type": "element", 624 | }, 625 | ], 626 | "name": "Routine", 627 | "type": "element", 628 | }, 629 | "class_id": "262fbaac4bd096afccf0fbf1fc161c89", 630 | "elem_id": "33cb4159377b63ce9db11667160e39df", 631 | } 632 | `; 633 | 634 | exports[`Program Class Methods findRoutine: Finds Target Routine 2`] = ` 635 | Routine { 636 | "_dom": Object { 637 | "attributes": Object { 638 | "Name": "bRoutine", 639 | "Type": "RLL", 640 | "Use": "Context", 641 | }, 642 | "elements": Array [ 643 | Object { 644 | "elements": Array [], 645 | "name": "RLLContent", 646 | "type": "element", 647 | }, 648 | ], 649 | "name": "Routine", 650 | "type": "element", 651 | }, 652 | "class_id": "262fbaac4bd096afccf0fbf1fc161c89", 653 | "elem_id": "33cb4159377b63ce9db11667160e39df", 654 | } 655 | `; 656 | 657 | exports[`Program Class Methods findRoutine: Finds Target Routine 3`] = ` 658 | Routine { 659 | "_dom": Object { 660 | "attributes": Object { 661 | "Name": "cRoutine", 662 | "Type": "RLL", 663 | "Use": "Context", 664 | }, 665 | "elements": Array [ 666 | Object { 667 | "elements": Array [], 668 | "name": "RLLContent", 669 | "type": "element", 670 | }, 671 | ], 672 | "name": "Routine", 673 | "type": "element", 674 | }, 675 | "class_id": "262fbaac4bd096afccf0fbf1fc161c89", 676 | "elem_id": "33cb4159377b63ce9db11667160e39df", 677 | } 678 | `; 679 | 680 | exports[`Program Class Methods findRoutine: Finds Target Routine 4`] = ` 681 | Routine { 682 | "_dom": Object { 683 | "attributes": Object { 684 | "Name": "cRoutine", 685 | "Type": "RLL", 686 | "Use": "Context", 687 | }, 688 | "elements": Array [ 689 | Object { 690 | "elements": Array [ 691 | Object { 692 | "attributes": Object { 693 | "Number": 0, 694 | "Type": "N", 695 | }, 696 | "elements": Array [ 697 | Object { 698 | "elements": Array [ 699 | Object { 700 | "cdata": "XIC(someTag)NOP();", 701 | "type": "cdata", 702 | }, 703 | ], 704 | "name": "Text", 705 | "type": "element", 706 | }, 707 | ], 708 | "name": "Rung", 709 | "type": "element", 710 | }, 711 | ], 712 | "name": "RLLContent", 713 | "type": "element", 714 | }, 715 | ], 716 | "name": "Routine", 717 | "type": "element", 718 | }, 719 | "class_id": "262fbaac4bd096afccf0fbf1fc161c89", 720 | "elem_id": "33cb4159377b63ce9db11667160e39df", 721 | } 722 | `; 723 | 724 | exports[`Program Class Methods findRoutine: Finds Target Routine 5`] = ` 725 | Program { 726 | "_dom": Object { 727 | "attributes": Object { 728 | "Class": "Standard", 729 | "Name": "testProg", 730 | "Type": "Normal", 731 | "Use": "Context", 732 | }, 733 | "elements": Array [ 734 | Object { 735 | "elements": Array [ 736 | Object { 737 | "attributes": Object { 738 | "Name": "aRoutine", 739 | "Type": "RLL", 740 | "Use": "Context", 741 | }, 742 | "elements": Array [ 743 | Object { 744 | "elements": Array [], 745 | "name": "RLLContent", 746 | "type": "element", 747 | }, 748 | ], 749 | "name": "Routine", 750 | "type": "element", 751 | }, 752 | Object { 753 | "attributes": Object { 754 | "Name": "bRoutine", 755 | "Type": "RLL", 756 | "Use": "Context", 757 | }, 758 | "elements": Array [ 759 | Object { 760 | "elements": Array [], 761 | "name": "RLLContent", 762 | "type": "element", 763 | }, 764 | ], 765 | "name": "Routine", 766 | "type": "element", 767 | }, 768 | Object { 769 | "attributes": Object { 770 | "Name": "cRoutine", 771 | "Type": "RLL", 772 | "Use": "Context", 773 | }, 774 | "elements": Array [ 775 | Object { 776 | "elements": Array [ 777 | Object { 778 | "attributes": Object { 779 | "Number": 0, 780 | "Type": "N", 781 | }, 782 | "elements": Array [ 783 | Object { 784 | "elements": Array [ 785 | Object { 786 | "cdata": "XIC(someTag)NOP();", 787 | "type": "cdata", 788 | }, 789 | ], 790 | "name": "Text", 791 | "type": "element", 792 | }, 793 | ], 794 | "name": "Rung", 795 | "type": "element", 796 | }, 797 | ], 798 | "name": "RLLContent", 799 | "type": "element", 800 | }, 801 | ], 802 | "name": "Routine", 803 | "type": "element", 804 | }, 805 | ], 806 | "name": "Routines", 807 | "type": "element", 808 | }, 809 | ], 810 | "name": "Program", 811 | "type": "element", 812 | }, 813 | "class_id": "bacac87817ce803505fcdfaf4360dc50", 814 | "elem_id": "33cb4159377b63ce9db11667160e39df", 815 | } 816 | `; 817 | 818 | exports[`Program Class Methods findRoutine: Finds Target Routine 6`] = ` 819 | Program { 820 | "_dom": Object { 821 | "attributes": Object { 822 | "Class": "Standard", 823 | "Name": "testProg", 824 | "Type": "Normal", 825 | "Use": "Context", 826 | }, 827 | "elements": Array [ 828 | Object { 829 | "elements": Array [ 830 | Object { 831 | "attributes": Object { 832 | "Name": "aRoutine", 833 | "Type": "RLL", 834 | "Use": "Context", 835 | }, 836 | "elements": Array [ 837 | Object { 838 | "elements": Array [ 839 | Object { 840 | "attributes": Object { 841 | "Number": 0, 842 | "Type": "N", 843 | }, 844 | "elements": Array [ 845 | Object { 846 | "elements": Array [ 847 | Object { 848 | "cdata": "XIC(anotherTag)NOP();", 849 | "type": "cdata", 850 | }, 851 | ], 852 | "name": "Text", 853 | "type": "element", 854 | }, 855 | ], 856 | "name": "Rung", 857 | "type": "element", 858 | }, 859 | ], 860 | "name": "RLLContent", 861 | "type": "element", 862 | }, 863 | ], 864 | "name": "Routine", 865 | "type": "element", 866 | }, 867 | Object { 868 | "attributes": Object { 869 | "Name": "bRoutine", 870 | "Type": "RLL", 871 | "Use": "Context", 872 | }, 873 | "elements": Array [ 874 | Object { 875 | "elements": Array [], 876 | "name": "RLLContent", 877 | "type": "element", 878 | }, 879 | ], 880 | "name": "Routine", 881 | "type": "element", 882 | }, 883 | Object { 884 | "attributes": Object { 885 | "Name": "cRoutine", 886 | "Type": "RLL", 887 | "Use": "Context", 888 | }, 889 | "elements": Array [ 890 | Object { 891 | "elements": Array [ 892 | Object { 893 | "attributes": Object { 894 | "Number": 0, 895 | "Type": "N", 896 | }, 897 | "elements": Array [ 898 | Object { 899 | "elements": Array [ 900 | Object { 901 | "cdata": "XIC(someTag)NOP();", 902 | "type": "cdata", 903 | }, 904 | ], 905 | "name": "Text", 906 | "type": "element", 907 | }, 908 | ], 909 | "name": "Rung", 910 | "type": "element", 911 | }, 912 | ], 913 | "name": "RLLContent", 914 | "type": "element", 915 | }, 916 | ], 917 | "name": "Routine", 918 | "type": "element", 919 | }, 920 | ], 921 | "name": "Routines", 922 | "type": "element", 923 | }, 924 | ], 925 | "name": "Program", 926 | "type": "element", 927 | }, 928 | "class_id": "bacac87817ce803505fcdfaf4360dc50", 929 | "elem_id": "33cb4159377b63ce9db11667160e39df", 930 | } 931 | `; 932 | 933 | exports[`Program Class Methods findTag: Finds Target Tag 1`] = ` 934 | Tag { 935 | "_dom": Object { 936 | "attributes": Object { 937 | "Class": "Standard", 938 | "DataType": "DINT", 939 | "Name": "aTag", 940 | "TagType": "Base", 941 | }, 942 | "elements": Array [], 943 | "name": "Tag", 944 | "type": "element", 945 | }, 946 | "class_id": "202108c6004705df7b5fd0add930619f", 947 | "elem_id": "33cb4159377b63ce9db11667160e39df", 948 | } 949 | `; 950 | 951 | exports[`Program Class Methods findTag: Finds Target Tag 2`] = ` 952 | Tag { 953 | "_dom": Object { 954 | "attributes": Object { 955 | "Class": "Standard", 956 | "DataType": "SINT", 957 | "Name": "bTag", 958 | "TagType": "Base", 959 | }, 960 | "elements": Array [], 961 | "name": "Tag", 962 | "type": "element", 963 | }, 964 | "class_id": "202108c6004705df7b5fd0add930619f", 965 | "elem_id": "33cb4159377b63ce9db11667160e39df", 966 | } 967 | `; 968 | 969 | exports[`Program Class Methods findTag: Finds Target Tag 3`] = ` 970 | Tag { 971 | "_dom": Object { 972 | "attributes": Object { 973 | "Class": "Standard", 974 | "DataType": "BOOL", 975 | "Name": "cTag", 976 | "TagType": "Base", 977 | }, 978 | "elements": Array [], 979 | "name": "Tag", 980 | "type": "element", 981 | }, 982 | "class_id": "202108c6004705df7b5fd0add930619f", 983 | "elem_id": "33cb4159377b63ce9db11667160e39df", 984 | } 985 | `; 986 | 987 | exports[`Program Class Methods findTag: Finds Target Tag 4`] = ` 988 | Program { 989 | "_dom": Object { 990 | "attributes": Object { 991 | "Class": "Standard", 992 | "Name": "testProg", 993 | "Type": "Normal", 994 | "Use": "Context", 995 | }, 996 | "elements": Array [ 997 | Object { 998 | "elements": Array [ 999 | Object { 1000 | "attributes": Object { 1001 | "Class": "Standard", 1002 | "DataType": "DINT", 1003 | "Name": "aTag", 1004 | "TagType": "Base", 1005 | }, 1006 | "elements": Array [], 1007 | "name": "Tag", 1008 | "type": "element", 1009 | }, 1010 | Object { 1011 | "attributes": Object { 1012 | "Class": "Standard", 1013 | "DataType": "SINT", 1014 | "Name": "bTag", 1015 | "TagType": "Base", 1016 | }, 1017 | "elements": Array [], 1018 | "name": "Tag", 1019 | "type": "element", 1020 | }, 1021 | Object { 1022 | "attributes": Object { 1023 | "Class": "Standard", 1024 | "DataType": "BOOL", 1025 | "Name": "eTag", 1026 | "TagType": "Base", 1027 | }, 1028 | "elements": Array [], 1029 | "name": "Tag", 1030 | "type": "element", 1031 | }, 1032 | ], 1033 | "name": "Tags", 1034 | "type": "element", 1035 | }, 1036 | ], 1037 | "name": "Program", 1038 | "type": "element", 1039 | }, 1040 | "class_id": "bacac87817ce803505fcdfaf4360dc50", 1041 | "elem_id": "33cb4159377b63ce9db11667160e39df", 1042 | } 1043 | `; 1044 | 1045 | exports[`Program Class Methods findTag: Finds Target Tag 5`] = ` 1046 | Tag { 1047 | "_dom": Object { 1048 | "attributes": Object { 1049 | "Class": "Standard", 1050 | "DataType": "BOOL", 1051 | "Name": "eTag", 1052 | "TagType": "Base", 1053 | }, 1054 | "elements": Array [], 1055 | "name": "Tag", 1056 | "type": "element", 1057 | }, 1058 | "class_id": "202108c6004705df7b5fd0add930619f", 1059 | "elem_id": "33cb4159377b63ce9db11667160e39df", 1060 | } 1061 | `; 1062 | 1063 | exports[`Program Class New Instance Initializes with Desired Document Model 1`] = ` 1064 | Program { 1065 | "_dom": Object { 1066 | "attributes": Object { 1067 | "Class": "Standard", 1068 | "Name": "ProgramName", 1069 | "Type": "Normal", 1070 | "Use": "Context", 1071 | }, 1072 | "elements": Array [], 1073 | "name": "Program", 1074 | "type": "element", 1075 | }, 1076 | "class_id": "bacac87817ce803505fcdfaf4360dc50", 1077 | "elem_id": "33cb4159377b63ce9db11667160e39df", 1078 | } 1079 | `; 1080 | 1081 | exports[`Program Class New Instance Initializes with Desired Document Model 2`] = ` 1082 | Program { 1083 | "_dom": Object { 1084 | "attributes": Object { 1085 | "Class": "Standard", 1086 | "Name": "ProgramName", 1087 | "Type": "Normal", 1088 | "Use": "Context", 1089 | }, 1090 | "elements": Array [ 1091 | Object { 1092 | "elements": Array [ 1093 | Object { 1094 | "cdata": "A Description", 1095 | "type": "cdata", 1096 | }, 1097 | ], 1098 | "name": "Description", 1099 | "type": "element", 1100 | }, 1101 | ], 1102 | "name": "Program", 1103 | "type": "element", 1104 | }, 1105 | "class_id": "bacac87817ce803505fcdfaf4360dc50", 1106 | "elem_id": "33cb4159377b63ce9db11667160e39df", 1107 | } 1108 | `; 1109 | 1110 | exports[`Program Class New Instance Initializes with Desired Document Model 3`] = ` 1111 | Program { 1112 | "_dom": Object { 1113 | "attributes": Object { 1114 | "Class": "Safety", 1115 | "Name": "ProgramName", 1116 | "Type": "Normal", 1117 | "Use": "Context", 1118 | }, 1119 | "elements": Array [ 1120 | Object { 1121 | "elements": Array [ 1122 | Object { 1123 | "cdata": "A Description", 1124 | "type": "cdata", 1125 | }, 1126 | ], 1127 | "name": "Description", 1128 | "type": "element", 1129 | }, 1130 | ], 1131 | "name": "Program", 1132 | "type": "element", 1133 | }, 1134 | "class_id": "bacac87817ce803505fcdfaf4360dc50", 1135 | "elem_id": "33cb4159377b63ce9db11667160e39df", 1136 | } 1137 | `; 1138 | -------------------------------------------------------------------------------- /src/program/index.js: -------------------------------------------------------------------------------- 1 | const Element = require("../element"); 2 | const Tag = require("../tag"); 3 | const Routine = require("../routine"); 4 | const { hash } = require("../utilities"); 5 | 6 | const PROGRAM_ID = hash("PROGRAM_ID_STRING"); 7 | 8 | class Program extends Element { 9 | constructor(name, description = null, safety = false) { 10 | if (typeof name !== "string") 11 | throw new Error( 12 | `Program constructor expected name of type instead got <${typeof name}>` 13 | ); 14 | 15 | if (description && typeof description !== "string") 16 | throw new Error( 17 | `Program constructor expected description of type instead got <${typeof description}>` 18 | ); 19 | 20 | if (safety && typeof safety !== "boolean") 21 | throw new Error( 22 | `Program constructor expected safety of type instead got <${typeof safety}>` 23 | ); 24 | 25 | let attributes = { 26 | Name: name, 27 | Type: "Normal", 28 | Class: safety ? "Safety" : "Standard", 29 | Use: "Context" 30 | }; 31 | 32 | let elements = []; 33 | 34 | /* eslint-disable indent */ 35 | if (description) 36 | elements.push({ 37 | type: "element", 38 | name: "Description", 39 | elements: [ 40 | { 41 | type: "cdata", 42 | cdata: description 43 | } 44 | ] 45 | }); 46 | /* eslint-enable indent */ 47 | 48 | // Call parent class constructor 49 | super({ 50 | type: "element", 51 | name: "Program", 52 | attributes, 53 | elements 54 | }); 55 | 56 | this.class_id = PROGRAM_ID; 57 | } 58 | 59 | /** 60 | * Adds tag to program instance 61 | * 62 | * @param {Tag} tag 63 | * @memberof Program 64 | */ 65 | addTag(tag) { 66 | if (!Tag.isTag(tag)) 67 | throw new Error(`addTag expected tag of type instead got <${typeof tag}>`); 68 | 69 | const tags = this.findOne("Tags"); 70 | 71 | /* eslint-disable indent */ 72 | if (tags) tags.append(tag); 73 | else 74 | this.append( 75 | new Element({ 76 | type: "element", 77 | name: "Tags", 78 | elements: [] 79 | }).append(tag) 80 | ); 81 | /* eslint-enable indent */ 82 | 83 | this._orderify(); 84 | return this; 85 | } 86 | 87 | /** 88 | * Adds routine to program instance 89 | * 90 | * @param {any} routine 91 | * @memberof Program 92 | */ 93 | addRoutine(routine) { 94 | if (!Routine.isRoutine(routine)) 95 | throw new Error( 96 | `addRoutine expected routine of type instead got <${typeof routine}>` 97 | ); 98 | 99 | const routines = this.findOne("Routines"); 100 | 101 | /* eslint-disable indent */ 102 | if (routines) routines.append(routine); 103 | else 104 | this.append( 105 | new Element({ 106 | type: "element", 107 | name: "Routines", 108 | elements: [] 109 | }).append(routine) 110 | ); 111 | /* eslint-enable indent */ 112 | 113 | this._orderify(); 114 | return this; 115 | } 116 | 117 | /** 118 | * Finds tag in the program instance that matches the name 119 | * 120 | * @param {string} name 121 | * @returns {Tag|null} 122 | * @memberof Program 123 | */ 124 | findTag(name) { 125 | if (typeof name !== "string") 126 | throw new Error( 127 | `findTag expected name of type instead got <${typeof name}>` 128 | ); 129 | 130 | const found = this.findOne("Tag", { Name: name }); 131 | if (!found) return null; 132 | 133 | const tag = new Tag(name, found.dom.attributes.DataType); 134 | tag._dom = found.dom; 135 | 136 | return tag; 137 | } 138 | 139 | /** 140 | * Finds routine in the program instance that matches the name 141 | * 142 | * @param {string} name 143 | * @returns {Routine|null} 144 | * @memberof Program 145 | */ 146 | findRoutine(name) { 147 | if (typeof name !== "string") 148 | throw new Error( 149 | `findRoutine expected name of type instead got <${typeof name}>` 150 | ); 151 | 152 | const found = this.findOne("Routine", { Name: name }); 153 | if (!found) return null; 154 | 155 | const routine = new Routine(name); 156 | routine._dom = found.dom; 157 | 158 | return routine; 159 | } 160 | 161 | /** 162 | * Puts the programs children in the correct order 163 | * 164 | * @private 165 | * @memberof Program 166 | */ 167 | _orderify() { 168 | const order = { 169 | Description: 1, 170 | Tags: 2, 171 | Routines: 3, 172 | ChildPrograms: 4 173 | }; 174 | 175 | this.dom.elements.sort((a, b) => { 176 | return order[a.name] - order[b.name]; 177 | }); 178 | } 179 | 180 | /** 181 | * Checks if an incoming object is a program instance 182 | * 183 | * @static 184 | * @param {any} program 185 | * @returns {boolean} 186 | * @memberof Tag 187 | */ 188 | static isProgram(program) { 189 | return program.class_id && program.class_id === PROGRAM_ID; 190 | } 191 | } 192 | 193 | module.exports = Program; 194 | -------------------------------------------------------------------------------- /src/program/program.spec.js: -------------------------------------------------------------------------------- 1 | const Program = require("./index"); 2 | const Tag = require("../tag"); 3 | const Routine = require("../routine"); 4 | const Rung = require("../rung"); 5 | 6 | describe("Program Class", () => { 7 | describe("New Instance", () => { 8 | it("Accepts Proper Input", () => { 9 | const fn = (name, desc = null, safety = false) => () => new Program(name, desc, safety); 10 | 11 | expect(fn("ProgramName")).not.toThrow(); 12 | expect(fn(null, "A Program Description")).toThrow(); 13 | expect(fn("ProgramName", 12)).toThrow(); 14 | expect(fn("ProgramName", "A Program Description")).not.toThrow(); 15 | expect(fn("ProgramName", "A Program Description", 12)).toThrow(); 16 | expect(fn("ProgramName", "A Program Description", true)).not.toThrow(); 17 | }); 18 | 19 | it("Initializes with Desired Document Model", () => { 20 | let program = new Program("ProgramName"); 21 | expect(program).toMatchSnapshot(); 22 | 23 | program = new Program("ProgramName", "A Description"); 24 | expect(program).toMatchSnapshot(); 25 | 26 | program = new Program("ProgramName", "A Description", true); 27 | expect(program).toMatchSnapshot(); 28 | }); 29 | }); 30 | 31 | describe("Methods", () => { 32 | let prog; 33 | 34 | beforeEach(() => { 35 | prog = new Program("testProg"); 36 | }); 37 | 38 | test("_orderify: Results in the Correct Order", () => { 39 | prog.dom.elements.push({ 40 | type: "element", 41 | name: "Tags", 42 | elements: [] 43 | }); 44 | 45 | prog.dom.elements.push({ 46 | type: "element", 47 | name: "Routines", 48 | elements: [] 49 | }); 50 | 51 | prog.dom.elements.push({ 52 | type: "element", 53 | name: "Description", 54 | elements: [] 55 | }); 56 | 57 | prog._orderify(); 58 | 59 | expect(prog).toMatchSnapshot(); 60 | }); 61 | 62 | test("addTag: Throws on Invalid Input", () => { 63 | const fn = tag => () => prog.addTag(tag); 64 | 65 | expect(fn(new Program("notATag"))).toThrow(); 66 | expect(fn("notATag")).toThrow(); 67 | }); 68 | 69 | test("addTag: Adds Tag Program Instance", () => { 70 | prog.addTag(new Tag("tag1", "BOOL")); 71 | expect(prog).toMatchSnapshot(); 72 | 73 | prog.addTag(new Tag("tag2", "DINT")); 74 | expect(prog).toMatchSnapshot(); 75 | 76 | prog.dom.elements.push({ 77 | type: "element", 78 | name: "Description", 79 | elements: [ 80 | { 81 | type: "cdata", 82 | cdata: "A Cool Program Description" 83 | } 84 | ] 85 | }); 86 | 87 | prog.addTag(new Tag("tag3", "SINT")); 88 | expect(prog).toMatchSnapshot(); 89 | 90 | const tag4 = new Tag("tag4", "REAL"); 91 | prog.addTag(tag4); 92 | expect(prog).toMatchSnapshot(); 93 | 94 | tag4.dom.attributes.Name = "not_tag4Anymore"; 95 | expect(prog).toMatchSnapshot(); 96 | }); 97 | 98 | test("addRoutine: Throws on Invalid Input", () => { 99 | const fn = rout => () => prog.addRoutine(rout); 100 | 101 | expect(fn(new Program("notATag"))).toThrow(); 102 | expect(fn("notATag")).toThrow(); 103 | }); 104 | 105 | test("addRoutine: Adds Routine Program Instance", () => { 106 | prog.addRoutine(new Routine("aRoutine")); 107 | expect(prog).toMatchSnapshot(); 108 | 109 | prog.addRoutine(new Routine("bRoutine")); 110 | expect(prog).toMatchSnapshot(); 111 | 112 | const rout = new Routine("cRoutine"); 113 | prog.addRoutine(rout); 114 | expect(prog).toMatchSnapshot(); 115 | 116 | rout.addRung(new Rung("XIC(someTag)NOP();")); 117 | expect(prog).toMatchSnapshot(); 118 | }); 119 | 120 | test("findTag: Throws on Invalid Input", () => { 121 | const fn = name => () => prog.findTag(name); 122 | 123 | expect(fn(new Program("notATag"))).toThrow(); 124 | expect(fn(12)).toThrow(); 125 | expect(fn("aTag")).not.toThrow(); 126 | }); 127 | 128 | test("findTag: Finds Target Tag", () => { 129 | prog.addTag(new Tag("aTag", "DINT")); 130 | prog.addTag(new Tag("bTag", "SINT")); 131 | 132 | const tag = new Tag("cTag", "BOOL"); 133 | prog.addTag(tag); 134 | 135 | expect(prog.findTag("aTag")).not.toBeNull(); 136 | expect(prog.findTag("aTag")).toMatchSnapshot(); 137 | expect(prog.findTag("bTag")).not.toBeNull(); 138 | expect(prog.findTag("bTag")).toMatchSnapshot(); 139 | expect(prog.findTag("cTag")).not.toBeNull(); 140 | expect(prog.findTag("cTag")).toMatchSnapshot(); 141 | expect(prog.findTag("dTag")).toBeNull(); 142 | 143 | prog.findTag("cTag").dom.attributes.Name = "eTag"; 144 | expect(prog).toMatchSnapshot(); 145 | expect(tag).toMatchSnapshot(); 146 | }); 147 | 148 | test("findRoutine: Throws on Invalid Input", () => { 149 | const fn = name => () => prog.findRoutine(name); 150 | 151 | expect(fn(new Program("notATag"))).toThrow(); 152 | expect(fn(12)).toThrow(); 153 | expect(fn("aRoutine")).not.toThrow(); 154 | }); 155 | 156 | test("findRoutine: Finds Target Routine", () => { 157 | prog.addRoutine(new Routine("aRoutine")); 158 | prog.addRoutine(new Routine("bRoutine")); 159 | 160 | const rout = new Routine("cRoutine"); 161 | prog.addRoutine(rout); 162 | 163 | expect(prog.findRoutine("aRoutine")).not.toBeNull(); 164 | expect(prog.findRoutine("aRoutine")).toMatchSnapshot(); 165 | expect(prog.findRoutine("bRoutine")).not.toBeNull(); 166 | expect(prog.findRoutine("bRoutine")).toMatchSnapshot(); 167 | expect(prog.findRoutine("cRoutine")).not.toBeNull(); 168 | expect(prog.findRoutine("cRoutine")).toMatchSnapshot(); 169 | expect(prog.findRoutine("dRoutine")).toBeNull(); 170 | 171 | rout.addRung(new Rung("XIC(someTag)NOP();")); 172 | expect(prog.findRoutine("cRoutine")).not.toBeNull(); 173 | expect(prog.findRoutine("cRoutine")).toMatchSnapshot(); 174 | expect(prog).toMatchSnapshot(); 175 | 176 | prog.findRoutine("aRoutine").addRung(new Rung("XIC(anotherTag)NOP();")); 177 | expect(prog).toMatchSnapshot(); 178 | }); 179 | }); 180 | 181 | describe("Static Methods", () => { 182 | test("isProgram: Returns Appropriate Judgement", () => { 183 | let program = new Program("ProgramName", "A Description"); 184 | expect(Program.isProgram(program)).toBeTruthy(); 185 | 186 | program = { notAProgram: 12 }; 187 | expect(Program.isProgram(program)).toBeFalsy(); 188 | 189 | program = 12; 190 | expect(Program.isProgram(program)).toBeFalsy(); 191 | }); 192 | }); 193 | }); 194 | -------------------------------------------------------------------------------- /src/routine/__snapshots__/routine.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Routine Class Methods addRung: Produces Desired Doc Model 1`] = ` 4 | Routine { 5 | "_dom": Object { 6 | "attributes": Object { 7 | "Name": "IamRoutine", 8 | "Type": "RLL", 9 | "Use": "Context", 10 | }, 11 | "elements": Array [ 12 | Object { 13 | "elements": Array [ 14 | Object { 15 | "attributes": Object { 16 | "Number": 0, 17 | "Type": "N", 18 | }, 19 | "elements": Array [ 20 | Object { 21 | "elements": Array [ 22 | Object { 23 | "cdata": "XIC(someTag)NOP();", 24 | "type": "cdata", 25 | }, 26 | ], 27 | "name": "Text", 28 | "type": "element", 29 | }, 30 | ], 31 | "name": "Rung", 32 | "type": "element", 33 | }, 34 | ], 35 | "name": "RLLContent", 36 | "type": "element", 37 | }, 38 | ], 39 | "name": "Routine", 40 | "type": "element", 41 | }, 42 | "class_id": "262fbaac4bd096afccf0fbf1fc161c89", 43 | "elem_id": "33cb4159377b63ce9db11667160e39df", 44 | } 45 | `; 46 | 47 | exports[`Routine Class Methods addRung: Produces Desired Doc Model 2`] = ` 48 | Routine { 49 | "_dom": Object { 50 | "attributes": Object { 51 | "Name": "IamRoutine", 52 | "Type": "RLL", 53 | "Use": "Context", 54 | }, 55 | "elements": Array [ 56 | Object { 57 | "elements": Array [ 58 | Object { 59 | "attributes": Object { 60 | "Number": 1, 61 | "Type": "N", 62 | }, 63 | "elements": Array [ 64 | Object { 65 | "elements": Array [ 66 | Object { 67 | "cdata": "XIC(someTag)NOP();", 68 | "type": "cdata", 69 | }, 70 | ], 71 | "name": "Text", 72 | "type": "element", 73 | }, 74 | ], 75 | "name": "Rung", 76 | "type": "element", 77 | }, 78 | ], 79 | "name": "RLLContent", 80 | "type": "element", 81 | }, 82 | ], 83 | "name": "Routine", 84 | "type": "element", 85 | }, 86 | "class_id": "262fbaac4bd096afccf0fbf1fc161c89", 87 | "elem_id": "33cb4159377b63ce9db11667160e39df", 88 | } 89 | `; 90 | 91 | exports[`Routine Class Methods addRung: Produces Desired Doc Model 3`] = ` 92 | Routine { 93 | "_dom": Object { 94 | "attributes": Object { 95 | "Name": "IamRoutine", 96 | "Type": "RLL", 97 | "Use": "Context", 98 | }, 99 | "elements": Array [ 100 | Object { 101 | "elements": Array [ 102 | Object { 103 | "attributes": Object { 104 | "Number": 0, 105 | "Type": "N", 106 | }, 107 | "elements": Array [ 108 | Object { 109 | "elements": Array [ 110 | Object { 111 | "cdata": "XIC(someTag)NOP();", 112 | "type": "cdata", 113 | }, 114 | ], 115 | "name": "Text", 116 | "type": "element", 117 | }, 118 | ], 119 | "name": "Rung", 120 | "type": "element", 121 | }, 122 | Object { 123 | "attributes": Object { 124 | "Number": 1, 125 | "Type": "N", 126 | }, 127 | "elements": Array [ 128 | Object { 129 | "elements": Array [ 130 | Object { 131 | "cdata": "XIC(anotherTag)NOP();", 132 | "type": "cdata", 133 | }, 134 | ], 135 | "name": "Text", 136 | "type": "element", 137 | }, 138 | ], 139 | "name": "Rung", 140 | "type": "element", 141 | }, 142 | ], 143 | "name": "RLLContent", 144 | "type": "element", 145 | }, 146 | ], 147 | "name": "Routine", 148 | "type": "element", 149 | }, 150 | "class_id": "262fbaac4bd096afccf0fbf1fc161c89", 151 | "elem_id": "33cb4159377b63ce9db11667160e39df", 152 | } 153 | `; 154 | 155 | exports[`Routine Class New Instance Initializes with Desired Document Model 1`] = ` 156 | Routine { 157 | "_dom": Object { 158 | "attributes": Object { 159 | "Name": "ARoutine", 160 | "Type": "RLL", 161 | "Use": "Context", 162 | }, 163 | "elements": Array [ 164 | Object { 165 | "elements": Array [], 166 | "name": "RLLContent", 167 | "type": "element", 168 | }, 169 | ], 170 | "name": "Routine", 171 | "type": "element", 172 | }, 173 | "class_id": "262fbaac4bd096afccf0fbf1fc161c89", 174 | "elem_id": "33cb4159377b63ce9db11667160e39df", 175 | } 176 | `; 177 | 178 | exports[`Routine Class New Instance Initializes with Desired Document Model 2`] = ` 179 | Routine { 180 | "_dom": Object { 181 | "attributes": Object { 182 | "Name": "ARoutine", 183 | "Type": "RLL", 184 | "Use": "Context", 185 | }, 186 | "elements": Array [ 187 | Object { 188 | "elements": Array [ 189 | Object { 190 | "cdata": "I am a description", 191 | "type": "cdata", 192 | }, 193 | ], 194 | "name": "Description", 195 | "type": "element", 196 | }, 197 | Object { 198 | "elements": Array [], 199 | "name": "RLLContent", 200 | "type": "element", 201 | }, 202 | ], 203 | "name": "Routine", 204 | "type": "element", 205 | }, 206 | "class_id": "262fbaac4bd096afccf0fbf1fc161c89", 207 | "elem_id": "33cb4159377b63ce9db11667160e39df", 208 | } 209 | `; 210 | -------------------------------------------------------------------------------- /src/routine/index.js: -------------------------------------------------------------------------------- 1 | const Element = require("../element"); 2 | const Rung = require("../rung"); 3 | const { hash } = require("../utilities"); 4 | 5 | const ROUTINE_ID = hash("ROUTINE_ID_STRING"); 6 | 7 | class Routine extends Element { 8 | constructor(name, description = null) { 9 | if (typeof name !== "string") 10 | throw new Error( 11 | `Routine constructor expected name of type instead got <${typeof name}>` 12 | ); 13 | 14 | if (description && typeof description !== "string") 15 | throw new Error( 16 | `Routine constructor expected description of type instead got <${typeof description}>` 17 | ); 18 | 19 | let attributes = { 20 | Name: name, 21 | Use: "Context", 22 | Type: "RLL" // Specify Ladder Logic 23 | }; 24 | 25 | let elements = []; 26 | 27 | /* eslint-disable indent */ 28 | if (description) 29 | elements.push({ 30 | type: "element", 31 | name: "Description", 32 | elements: [ 33 | { 34 | type: "cdata", 35 | cdata: description 36 | } 37 | ] 38 | }); 39 | 40 | elements.push({ 41 | type: "element", 42 | name: "RLLContent", // Routine content 43 | elements: [] 44 | }); 45 | /* eslint-enable indent */ 46 | 47 | // Call parent class constructor 48 | super({ 49 | type: "element", 50 | name: "Routine", 51 | attributes, 52 | elements 53 | }); 54 | 55 | this.class_id = ROUTINE_ID; 56 | } 57 | 58 | /** 59 | * Adds a rung to the ladder content 60 | * 61 | * @param {Rung} rung 62 | * @memberof Routine 63 | */ 64 | addRung(rung) { 65 | if (!Rung.isRung(rung)) 66 | throw new Error(`addRung expected rung of type instead got <${typeof rung}>`); 67 | 68 | const rllContent = this.findOne("RLLContent"); 69 | const rungNum = rllContent.dom.elements.length; 70 | 71 | rung.dom.attributes.Number = rungNum; 72 | 73 | rllContent.append(rung); 74 | return this; 75 | } 76 | 77 | /** 78 | * Checks if an incoming object is a Routine instance 79 | * 80 | * @static 81 | * @param {any} routine 82 | * @returns {boolean} 83 | * @memberof Tag 84 | */ 85 | static isRoutine(routine) { 86 | return routine.class_id && routine.class_id === ROUTINE_ID; 87 | } 88 | } 89 | 90 | module.exports = Routine; 91 | -------------------------------------------------------------------------------- /src/routine/routine.spec.js: -------------------------------------------------------------------------------- 1 | const Routine = require("./index"); 2 | const Element = require("../element"); 3 | const Rung = require("../rung"); 4 | 5 | describe("Routine Class", () => { 6 | describe("New Instance", () => { 7 | it("Accepts Proper Input", () => { 8 | const fn = (name, desc = null) => () => new Routine(name, desc); 9 | 10 | expect(fn()).toThrow(); 11 | expect(fn(12)).toThrow(); 12 | expect(fn("IamRoutine")).not.toThrow(); 13 | expect(fn("IamRoutine", 12)).toThrow(); 14 | expect(fn("IamRoutine", "I am a decription")).not.toThrow(); 15 | }); 16 | 17 | it("Initializes with Desired Document Model", () => { 18 | let routine = new Routine("ARoutine"); 19 | expect(routine).toMatchSnapshot(); 20 | 21 | routine = new Routine("ARoutine", "I am a description"); 22 | expect(routine).toMatchSnapshot(); 23 | }); 24 | }); 25 | 26 | describe("Methods", () => { 27 | let rout; 28 | 29 | beforeEach(() => { 30 | rout = new Routine("IamRoutine"); 31 | }); 32 | 33 | test("addRung: Throws on Invalid Input", () => { 34 | const fn = rung => () => rout.addRung(rung); 35 | 36 | expect(fn(new Rung("XIC(someTag)NOP();"))).not.toThrow(); 37 | expect(fn(new Element({ type: "element", name: "el" }))).toThrow(); 38 | }); 39 | 40 | test("addRung: Produces Desired Doc Model", () => { 41 | const r1 = new Rung("XIC(someTag)NOP();"); 42 | rout.addRung(r1); 43 | expect(rout).toMatchSnapshot(); 44 | 45 | r1.dom.attributes.Number = 1; 46 | expect(rout).toMatchSnapshot(); 47 | 48 | r1.dom.attributes.Number = 0; 49 | rout.addRung(new Rung("XIC(anotherTag)NOP();")); 50 | expect(rout).toMatchSnapshot(); 51 | }); 52 | }); 53 | 54 | describe("Static Methods", () => { 55 | test("isRoutine: Returns Appropriate Judgement", () => { 56 | let routine = new Routine("ARoutine"); 57 | expect(Routine.isRoutine(routine)).toBeTruthy(); 58 | 59 | routine = { notARoutine: 12 }; 60 | expect(Routine.isRoutine(routine)).toBeFalsy(); 61 | 62 | routine = 12; 63 | expect(Routine.isRoutine(routine)).toBeFalsy(); 64 | }); 65 | }); 66 | }); 67 | -------------------------------------------------------------------------------- /src/rung/__snapshots__/rung.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Rung Class New Instance Initializes with Desired Document Model 1`] = ` 4 | Rung { 5 | "_dom": Object { 6 | "attributes": Object { 7 | "Number": 12, 8 | "Type": "N", 9 | }, 10 | "elements": Array [ 11 | Object { 12 | "elements": Array [ 13 | Object { 14 | "cdata": "XIC(someTag)NOP();", 15 | "type": "cdata", 16 | }, 17 | ], 18 | "name": "Text", 19 | "type": "element", 20 | }, 21 | ], 22 | "name": "Rung", 23 | "type": "element", 24 | }, 25 | "class_id": "b49e5b79e8662ca3b6e884d3057719d0", 26 | "elem_id": "33cb4159377b63ce9db11667160e39df", 27 | } 28 | `; 29 | 30 | exports[`Rung Class New Instance Initializes with Desired Document Model 2`] = ` 31 | Rung { 32 | "_dom": Object { 33 | "attributes": Object { 34 | "Number": 12, 35 | "Type": "N", 36 | }, 37 | "elements": Array [ 38 | Object { 39 | "elements": Array [ 40 | Object { 41 | "cdata": "I am a comment", 42 | "type": "cdata", 43 | }, 44 | ], 45 | "name": "Comment", 46 | "type": "element", 47 | }, 48 | Object { 49 | "elements": Array [ 50 | Object { 51 | "cdata": "XIC(someTag)NOP();", 52 | "type": "cdata", 53 | }, 54 | ], 55 | "name": "Text", 56 | "type": "element", 57 | }, 58 | ], 59 | "name": "Rung", 60 | "type": "element", 61 | }, 62 | "class_id": "b49e5b79e8662ca3b6e884d3057719d0", 63 | "elem_id": "33cb4159377b63ce9db11667160e39df", 64 | } 65 | `; 66 | -------------------------------------------------------------------------------- /src/rung/index.js: -------------------------------------------------------------------------------- 1 | const Element = require("../element"); 2 | const { hash } = require("../utilities"); 3 | 4 | const RUNG_ID = hash("RUNG_ID_STRING"); 5 | 6 | class Rung extends Element { 7 | constructor(logic, num = 0, comment = null) { 8 | if (typeof logic !== "string") 9 | throw new Error( 10 | `Rung constructor expected logic of type instead got <${typeof logic}>` 11 | ); 12 | 13 | if (typeof num !== "number") 14 | throw new Error( 15 | `Rung constructor expected num of type instead got <${typeof num}>` 16 | ); 17 | 18 | if (comment && typeof comment !== "string") 19 | throw new Error( 20 | `Rung constructor expected comment of type instead got <${typeof comment}>` 21 | ); 22 | 23 | let attributes = { 24 | Number: num, 25 | Type: "N" 26 | }; 27 | 28 | let elements = []; 29 | 30 | /* eslint-disable indent */ 31 | if (comment) 32 | elements.push({ 33 | type: "element", 34 | name: "Comment", 35 | elements: [ 36 | { 37 | type: "cdata", 38 | cdata: comment 39 | } 40 | ] 41 | }); 42 | 43 | elements.push({ 44 | type: "element", 45 | name: "Text", // rung content 46 | elements: [ 47 | { 48 | type: "cdata", 49 | cdata: logic 50 | } 51 | ] 52 | }); 53 | /* eslint-enable indent */ 54 | 55 | // Call parent class constructor 56 | super({ 57 | type: "element", 58 | name: "Rung", 59 | attributes, 60 | elements 61 | }); 62 | 63 | this.class_id = RUNG_ID; 64 | } 65 | 66 | /** 67 | * Checks if an incoming object is a rung instance 68 | * 69 | * @static 70 | * @param {any} rung 71 | * @returns {boolean} 72 | * @memberof Tag 73 | */ 74 | static isRung(rung) { 75 | return rung.class_id && rung.class_id === RUNG_ID; 76 | } 77 | } 78 | 79 | module.exports = Rung; 80 | -------------------------------------------------------------------------------- /src/rung/rung.spec.js: -------------------------------------------------------------------------------- 1 | const Rung = require("./index"); 2 | const Element = require("../element"); 3 | 4 | describe("Rung Class", () => { 5 | describe("New Instance", () => { 6 | it("Accepts Proper Input", () => { 7 | const fn = (logic, num, comment = null) => () => new Rung(logic, num, comment); 8 | 9 | expect(fn("XIC(someTag)NOP();")).not.toThrow(); 10 | expect(fn("XIC(someTag)NOP();", "notANum")).toThrow(); 11 | expect(fn(null, 12)).toThrow(); 12 | expect(fn("XIC(someTag)NOP();", 12)).not.toThrow(); 13 | expect(fn("XIC(someTag)NOP();", 12, 12)).toThrow(); 14 | expect(fn("XIC(someTag)NOP();", 12, "I am a Comment")).not.toThrow(); 15 | }); 16 | 17 | it("Initializes with Desired Document Model", () => { 18 | let rung = new Rung("XIC(someTag)NOP();", 12); 19 | expect(rung).toMatchSnapshot(); 20 | 21 | rung = new Rung("XIC(someTag)NOP();", 12, "I am a comment"); 22 | expect(rung).toMatchSnapshot(); 23 | }); 24 | }); 25 | 26 | describe("Static Methods", () => { 27 | test("isRung: Returns Appropriate Judgement", () => { 28 | let rung = new Rung("XIC(someTag)NOP();"); 29 | expect(Rung.isRung(rung)).toBeTruthy(); 30 | 31 | rung = { notARung: 12 }; 32 | expect(Rung.isRung(rung)).toBeFalsy(); 33 | 34 | rung = 12; 35 | expect(Rung.isRung(rung)).toBeFalsy(); 36 | 37 | rung = new Element({ type: "element", name: "el" }); 38 | expect(Rung.isRung(rung)).toBeFalsy(); 39 | }); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /src/tag/__snapshots__/tag.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Tag Class New Instance Initializes with Desired Document Model 1`] = ` 4 | Tag { 5 | "_dom": Object { 6 | "attributes": Object { 7 | "Class": "Standard", 8 | "DataType": "DINT", 9 | "Name": "tagName", 10 | "TagType": "Base", 11 | }, 12 | "elements": Array [], 13 | "name": "Tag", 14 | "type": "element", 15 | }, 16 | "class_id": "202108c6004705df7b5fd0add930619f", 17 | "elem_id": "33cb4159377b63ce9db11667160e39df", 18 | } 19 | `; 20 | 21 | exports[`Tag Class New Instance Initializes with Desired Document Model 2`] = ` 22 | Tag { 23 | "_dom": Object { 24 | "attributes": Object { 25 | "Class": "Standard", 26 | "DataType": "DINT", 27 | "Name": "tagName", 28 | "TagType": "Base", 29 | }, 30 | "elements": Array [ 31 | Object { 32 | "elements": Array [ 33 | Object { 34 | "cdata": "A Description", 35 | "type": "cdata", 36 | }, 37 | ], 38 | "name": "Description", 39 | "type": "element", 40 | }, 41 | ], 42 | "name": "Tag", 43 | "type": "element", 44 | }, 45 | "class_id": "202108c6004705df7b5fd0add930619f", 46 | "elem_id": "33cb4159377b63ce9db11667160e39df", 47 | } 48 | `; 49 | 50 | exports[`Tag Class New Instance Initializes with Desired Document Model 3`] = ` 51 | Tag { 52 | "_dom": Object { 53 | "attributes": Object { 54 | "AliasFor": "anotherTag", 55 | "Class": "Standard", 56 | "DataType": "DINT", 57 | "Name": "tagName", 58 | "TagType": "Alias", 59 | }, 60 | "elements": Array [ 61 | Object { 62 | "elements": Array [ 63 | Object { 64 | "cdata": "A Description", 65 | "type": "cdata", 66 | }, 67 | ], 68 | "name": "Description", 69 | "type": "element", 70 | }, 71 | ], 72 | "name": "Tag", 73 | "type": "element", 74 | }, 75 | "class_id": "202108c6004705df7b5fd0add930619f", 76 | "elem_id": "33cb4159377b63ce9db11667160e39df", 77 | } 78 | `; 79 | 80 | exports[`Tag Class New Instance Initializes with Desired Document Model 4`] = ` 81 | Tag { 82 | "_dom": Object { 83 | "attributes": Object { 84 | "AliasFor": "anotherTag", 85 | "Class": "Safety", 86 | "DataType": "DINT", 87 | "Name": "tagName", 88 | "TagType": "Alias", 89 | }, 90 | "elements": Array [ 91 | Object { 92 | "elements": Array [ 93 | Object { 94 | "cdata": "A Description", 95 | "type": "cdata", 96 | }, 97 | ], 98 | "name": "Description", 99 | "type": "element", 100 | }, 101 | ], 102 | "name": "Tag", 103 | "type": "element", 104 | }, 105 | "class_id": "202108c6004705df7b5fd0add930619f", 106 | "elem_id": "33cb4159377b63ce9db11667160e39df", 107 | } 108 | `; 109 | 110 | exports[`Tag Class New Instance Initializes with Desired Document Model 5`] = ` 111 | Tag { 112 | "_dom": Object { 113 | "attributes": Object { 114 | "AliasFor": "anotherTag", 115 | "Class": "Safety", 116 | "DataType": "DINT", 117 | "Dimensions": "[12]", 118 | "Name": "tagName", 119 | "TagType": "Alias", 120 | }, 121 | "elements": Array [ 122 | Object { 123 | "elements": Array [ 124 | Object { 125 | "cdata": "A Description", 126 | "type": "cdata", 127 | }, 128 | ], 129 | "name": "Description", 130 | "type": "element", 131 | }, 132 | ], 133 | "name": "Tag", 134 | "type": "element", 135 | }, 136 | "class_id": "202108c6004705df7b5fd0add930619f", 137 | "elem_id": "33cb4159377b63ce9db11667160e39df", 138 | } 139 | `; 140 | -------------------------------------------------------------------------------- /src/tag/index.js: -------------------------------------------------------------------------------- 1 | const Element = require("../element"); 2 | const { hash } = require("../utilities"); 3 | 4 | const TAG_ID = hash("TAG_ID_STRING"); 5 | 6 | class Tag extends Element { 7 | constructor( 8 | name, 9 | datatype, 10 | description = null, 11 | alias = null, 12 | safety = false, 13 | dimension = null 14 | ) { 15 | if (typeof name !== "string") 16 | throw new Error( 17 | `Tag constructor expected name of type instead got <${typeof name}>` 18 | ); 19 | 20 | if (typeof datatype !== "string") 21 | throw new Error( 22 | `Tag constructor expected datatype of type instead got <${typeof datatype}>` 23 | ); 24 | 25 | if (description && typeof description !== "string") 26 | throw new Error( 27 | `Tag constructor expected description of type instead got <${typeof description}>` 28 | ); 29 | 30 | if (alias && typeof alias !== "string") 31 | throw new Error( 32 | `Tag constructor expected alias of type instead got <${typeof alias}>` 33 | ); 34 | 35 | if (safety && typeof safety !== "boolean") 36 | throw new Error( 37 | `Tag constructor expected safety of type instead got <${typeof safety}>` 38 | ); 39 | 40 | if (dimension && typeof dimension !== "string") 41 | throw new Error( 42 | `Tag constructor expected dimension of type instead got <${typeof dimension}>` 43 | ); 44 | 45 | let attributes = { 46 | Name: name, 47 | DataType: datatype, 48 | TagType: alias ? "Alias" : "Base", 49 | Class: safety ? "Safety" : "Standard" 50 | }; 51 | 52 | if (alias) attributes.AliasFor = alias; 53 | if (dimension) attributes.Dimensions = dimension; 54 | 55 | let elements = []; 56 | 57 | /* eslint-disable indent */ 58 | if (description) 59 | elements.push({ 60 | type: "element", 61 | name: "Description", 62 | elements: [ 63 | { 64 | type: "cdata", 65 | cdata: description 66 | } 67 | ] 68 | }); 69 | /* eslint-enable indent */ 70 | 71 | // Call parent class constructor 72 | super({ 73 | type: "element", 74 | name: "Tag", 75 | attributes, 76 | elements 77 | }); 78 | 79 | this.class_id = TAG_ID; 80 | } 81 | 82 | /** 83 | * Checks if an incoming object is a tag instance 84 | * 85 | * @static 86 | * @param {any} tag 87 | * @returns {boolean} 88 | * @memberof Tag 89 | */ 90 | static isTag(tag) { 91 | return tag.class_id && tag.class_id === TAG_ID; 92 | } 93 | } 94 | 95 | module.exports = Tag; 96 | -------------------------------------------------------------------------------- /src/tag/tag.spec.js: -------------------------------------------------------------------------------- 1 | const Tag = require("./index"); 2 | 3 | describe("Tag Class", () => { 4 | describe("New Instance", () => { 5 | it("Accepts Proper Input", () => { 6 | const fn = ( 7 | name, 8 | datatype, 9 | desc = null, 10 | alias = null, 11 | safety = false, 12 | dim = null 13 | ) => () => new Tag(name, datatype, desc, alias, safety, dim); 14 | 15 | expect(fn("tagName")).toThrow(); 16 | expect(fn(null, "DINT")).toThrow(); 17 | expect(fn("tagName", "DINT")).not.toThrow(); 18 | expect(fn("tagName", "DINT", 12)).toThrow(); 19 | expect(fn("tagName", "DINT", "A Description")).not.toThrow(); 20 | expect(fn("tagName", "DINT", "A Description", 12)).toThrow(); 21 | expect(fn("tagName", "DINT", "A Description", "anotherTag")).not.toThrow(); 22 | expect(fn("tagName", "DINT", "A Description", "anotherTag", 12)).toThrow(); 23 | expect(fn("tagName", "DINT", "A Description", "anotherTag", true)).not.toThrow(); 24 | expect(fn("tagName", "DINT", "A Description", "anotherTag", true, 12)).toThrow(); 25 | expect( 26 | fn("tagName", "DINT", "A Description", "anotherTag", true, "[10]") 27 | ).not.toThrow(); 28 | }); 29 | 30 | it("Initializes with Desired Document Model", () => { 31 | let tag = new Tag("tagName", "DINT"); 32 | expect(tag).toMatchSnapshot(); 33 | 34 | tag = new Tag("tagName", "DINT", "A Description"); 35 | expect(tag).toMatchSnapshot(); 36 | 37 | tag = new Tag("tagName", "DINT", "A Description", "anotherTag"); 38 | expect(tag).toMatchSnapshot(); 39 | 40 | tag = new Tag("tagName", "DINT", "A Description", "anotherTag", true); 41 | expect(tag).toMatchSnapshot(); 42 | 43 | tag = new Tag("tagName", "DINT", "A Description", "anotherTag", true, "[12]"); 44 | expect(tag).toMatchSnapshot(); 45 | }); 46 | }); 47 | 48 | describe("Static Methods", () => { 49 | test("isTag: Returns Appropriate Judgement", () => { 50 | let tag = new Tag("tagName", "DINT"); 51 | expect(Tag.isTag(tag)).toBeTruthy(); 52 | 53 | tag = { notATag: 12 }; 54 | expect(Tag.isTag(tag)).toBeFalsy(); 55 | 56 | tag = 12; 57 | expect(Tag.isTag(tag)).toBeFalsy(); 58 | }); 59 | }); 60 | }); 61 | -------------------------------------------------------------------------------- /src/utilities/index.js: -------------------------------------------------------------------------------- 1 | const crypto = require("crypto"); 2 | 3 | /** 4 | * Generates Unique ID for Each Instance 5 | * based on the Generated EPATH 6 | * 7 | * @param {buffer} input - EPATH of Tag 8 | * @returns {string} hash 9 | */ 10 | const hash = input => { 11 | return crypto 12 | .createHash("md5") 13 | .update(input) 14 | .digest("hex"); 15 | }; 16 | 17 | module.exports = { hash }; 18 | -------------------------------------------------------------------------------- /src/utilities/utilities.spec.js: -------------------------------------------------------------------------------- 1 | const { hash } = require("./index"); 2 | 3 | describe("Utilities", () => { 4 | test("hash: Produces Correct Output", () => { 5 | expect(hash("hello world") === hash("hello world")).toBeTruthy(); 6 | expect(hash("I am a document") === hash("I am a document")).toBeTruthy(); 7 | }); 8 | }); 9 | --------------------------------------------------------------------------------