├── .eslintignore ├── .eslintrc.json ├── .github └── workflows │ └── nodejs.yml ├── .gitignore ├── .npmignore ├── .vscode ├── launch.json └── settings.json ├── CHANGELOG.md ├── LICENSE ├── README.md ├── examples ├── Alert.json ├── Alert.svelte ├── Button.json ├── Button.svelte ├── SlotList.json └── SlotList.svelte ├── index.js ├── lib ├── detector.js ├── helpers.js ├── jsdoc.js ├── options.js ├── parser.js ├── utils.js └── v3 │ ├── events.js │ ├── parser.js │ ├── script.js │ ├── template.js │ └── v3-utils.js ├── package.json ├── scripts └── generate-examples.js ├── test ├── integration │ ├── detector │ │ ├── detector.spec.js │ │ ├── empty.svelte │ │ ├── v2 │ │ │ ├── elseif.svelte │ │ │ ├── exportDefault.svelte │ │ │ └── ref.svelte │ │ └── v3 │ │ │ ├── context.svelte │ │ │ ├── contextAndScript.svelte │ │ │ ├── elseif.svelte │ │ │ └── letInSlot.svelte │ └── parse │ │ ├── basicV2.svelte │ │ ├── basicV3.svelte │ │ └── parse.spec.js ├── svelte2 │ └── integration │ │ ├── basics │ │ ├── basic.description.svelte │ │ └── basic.spec.js │ │ ├── data │ │ ├── data.description.svelte │ │ ├── data.plain.svelte │ │ ├── data.spec.js │ │ ├── data.typeFromDescription.svelte │ │ └── data.typeFromValue.svelte │ │ ├── events │ │ ├── event.markup.fire.svelte │ │ ├── event.markup.modificators.svelte │ │ ├── event.markup.propogate.svelte │ │ ├── event.method.fire.identifier.svelte │ │ ├── event.method.fire.svelte │ │ └── events.spec.js │ │ ├── failure │ │ ├── failure.invalid.js.svelte │ │ └── failure.spec.js │ │ ├── locations │ │ ├── locations.spec.js │ │ └── main.svelte │ │ ├── overall │ │ ├── main.svelte │ │ ├── overall.main.doc.json │ │ └── overall.main.spec.js │ │ └── store │ │ ├── store.get.markup.svelte │ │ ├── store.markup.svelte │ │ ├── store.set.markup.svelte │ │ └── store.spec.js ├── svelte3 │ └── integration │ │ ├── basic │ │ ├── basic.name.svelte │ │ ├── basic.name.wc.svelte │ │ └── basic.spec.js │ │ ├── bind │ │ ├── bind.declared.svelte │ │ ├── bind.exported.svelte │ │ ├── bind.multiple.svelte │ │ ├── bind.named.svelte │ │ ├── bind.simple.svelte │ │ └── bind.spec.js │ │ ├── components │ │ ├── components.import.aliace.svelte │ │ ├── components.import.svelte │ │ ├── components.importStar.svelte │ │ ├── components.importable.js │ │ ├── components.lowercase.svelte │ │ ├── components.nested.svelte │ │ ├── components.notdefault.svelte │ │ └── components.spec.js │ │ ├── computed │ │ ├── computed.declaration.svelte │ │ ├── computed.expression.svelte │ │ └── computed.spec.js │ │ ├── data │ │ ├── const.importable.js │ │ ├── data.const.svelte │ │ ├── data.export.aliace.svelte │ │ ├── data.export.functionExpression.svelte │ │ ├── data.export.many.svelte │ │ ├── data.exportNamed.many.svelte │ │ ├── data.import.aliace.svelte │ │ ├── data.import.default.svelte │ │ ├── data.import.many.svelte │ │ ├── data.multiple.svelte │ │ ├── data.objectPattern.svelte │ │ ├── data.private.svelte │ │ ├── data.public.svelte │ │ ├── data.spec.js │ │ ├── data.static.svelte │ │ └── data.types.svelte │ │ ├── events │ │ ├── event.dispatcher.arrayIdentifier.svelte │ │ ├── event.dispatcher.custom.svelte │ │ ├── event.dispatcher.customConstructor.svelte │ │ ├── event.dispatcher.default.svelte │ │ ├── event.dispatcher.externalCallback.svelte │ │ ├── event.dispatcher.identifier.svelte │ │ ├── event.dispatcher.importedIdentifier.svelte │ │ ├── event.dispatcher.insideMethod.svelte │ │ ├── event.dispatcher.markup.svelte │ │ ├── event.markup.dispatcher.custom.svelte │ │ ├── event.markup.dispatcher.customConstructor.svelte │ │ ├── event.markup.dispatcher.default.function.svelte │ │ ├── event.markup.dispatcher.default.svelte │ │ ├── event.markup.handleAndPropogate.svelte │ │ ├── event.markup.modificators.svelte │ │ ├── event.markup.propogate.svelte │ │ ├── events.spec.js │ │ └── sharedEvents.importable.js │ │ ├── globalComment │ │ ├── globalComment.markup.svelte │ │ ├── globalComment.nested.svelte │ │ ├── globalComment.noComment.svelte │ │ ├── globalComment.script.svelte │ │ └── globalComment.spec.js │ │ ├── locations │ │ ├── locations.multiscripts.svelte │ │ └── locations.spec.js │ │ ├── methods │ │ ├── method.private.svelte │ │ ├── method.public.svelte │ │ ├── method.typeInference.svelte │ │ └── methods.spec.js │ │ ├── refs │ │ ├── ref.component.svelte │ │ ├── ref.declared.svelte │ │ ├── ref.simple.svelte │ │ └── refs.spec.js │ │ └── slots │ │ ├── slot.comments.svelte │ │ ├── slot.default.svelte │ │ ├── slot.named.svelte │ │ ├── slot.parameters.svelte │ │ └── slots.spec.js └── unit │ ├── helpers │ ├── helpers.spec.js │ └── utils.spec.js │ ├── jsdoc │ └── jsdoc.spec.js │ └── options │ └── options.spec.js ├── typings.d.ts └── yarn.lock /.eslintignore: -------------------------------------------------------------------------------- 1 | /test/**/*.importable.js -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "eslint:recommended", 3 | "parserOptions": { 4 | "ecmaVersion": 2018, 5 | "ecmaFeatures": { 6 | "jsx": false 7 | } 8 | }, 9 | "env": { 10 | "node": true, 11 | "mocha": true, 12 | "es6": true 13 | }, 14 | "globals": { 15 | "expect": false 16 | }, 17 | "plugins": [ 18 | "chai-expect" 19 | ], 20 | "settings": { 21 | }, 22 | "rules": { 23 | "curly": ["error", "all"], 24 | "no-var": "error", 25 | "indent": ["error", 4, { "SwitchCase": 1 }], 26 | "semi": ["error", "always"], 27 | "object-curly-spacing": ["error", "always"], 28 | "comma-dangle": ["error", "only-multiline"], 29 | "space-before-function-paren": [ 30 | "error", 31 | { 32 | "anonymous": "always", 33 | "named": "never", 34 | "asyncArrow": "always" 35 | } 36 | ], 37 | "padding-line-between-statements": [ 38 | "error", 39 | { 40 | "blankLine": "always", 41 | "prev": ["directive", "import", "const", "let", "block-like"], 42 | "next": "*" 43 | }, 44 | { 45 | "blankLine": "always", 46 | "prev": "*", 47 | "next": ["export", "return", "throw", "block-like"] 48 | }, 49 | { 50 | "blankLine": "any", 51 | "prev": "import", 52 | "next": "import" 53 | }, 54 | { 55 | "blankLine": "any", 56 | "prev": "const", 57 | "next": "const" 58 | }, 59 | { 60 | "blankLine": "any", 61 | "prev": "let", 62 | "next": "let" 63 | } 64 | ], 65 | "padded-blocks": ["error", "never"], 66 | "prefer-rest-params": "error", 67 | "prefer-spread": "error", 68 | "prefer-const": "error", 69 | "chai-expect/terminating-properties": 1, 70 | "no-unused-expressions": "off" 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /.github/workflows/nodejs.yml: -------------------------------------------------------------------------------- 1 | name: Node CI 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - master 7 | push: 8 | 9 | 10 | jobs: 11 | lint: 12 | runs-on: ubuntu-latest 13 | strategy: 14 | matrix: 15 | node-version: [12.x] 16 | 17 | steps: 18 | - uses: actions/checkout@v1 19 | - name: Use Node.js ${{ matrix.node-version }} 20 | uses: actions/setup-node@v1 21 | with: 22 | node-version: ${{ matrix.node-version }} 23 | - name: npm install, build, and test 24 | run: | 25 | npm install 26 | npm run test 27 | env: 28 | CI: true 29 | 30 | tests: 31 | runs-on: ubuntu-latest 32 | 33 | strategy: 34 | matrix: 35 | node-version: [12.x, 14.x] 36 | 37 | steps: 38 | - uses: actions/checkout@v1 39 | - name: Use Node.js ${{ matrix.node-version }} 40 | uses: actions/setup-node@v1 41 | with: 42 | node-version: ${{ matrix.node-version }} 43 | - name: npm install, build, and test 44 | run: | 45 | npm install 46 | npm run test 47 | env: 48 | CI: true 49 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | *.log 3 | /build 4 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | /test 2 | /examples 3 | /scripts 4 | .eslintrc.json 5 | .gitignore 6 | yarn.lock 7 | .github 8 | .vscode 9 | yarn-error.log -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "request": "launch", 9 | "name": "Debug Mocha Test", 10 | "type": "node", 11 | "program": "${workspaceRoot}/node_modules/mocha/bin/_mocha", 12 | "args": [ 13 | "./test/**/*.spec.js", 14 | "--colors", 15 | "--no-timeouts" 16 | ], 17 | "cwd": "${workspaceRoot}", 18 | "runtimeExecutable": null, 19 | "env": {} 20 | } 21 | ] 22 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.eol": "\n" 3 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | All notable changes to the "svelte-intellisense" extension will be documented in this file. 3 | 4 | Check [Keep a Changelog](http://keepachangelog.com/) for recommendations on how to structure this file. 5 | 6 | ## UNPUBLISHED 7 | 8 | ## [4.2.0] 14.12.2021 9 | 10 | - 🔒 **[Fixed]** Upgrade all dependecies to latest version to solve known vulnarability issues. 11 | - ✔ **[Added]** Add support ES6 default value assignment for method parameter [Issue #75](https://github.com/alexprey/sveltedoc-parser/issues/75). Thanks for @ekhaled. 12 | - ✔ **[Added]** Add support of method parsing when it assigned to identifier [Issue #78](https://github.com/alexprey/sveltedoc-parser/issues/78). Thanks for @ekhaled. 13 | - ✔ **[Added]** Extend typings to support `self` and `trusted` event modifiers [Issue #80]. 14 | - ✔ **[Added]** Introduce `JSDocTypeFunction` to support functions types in variable definitions and provide details about function parameters and methods. 15 | - ✔ **[Added]** Extend `JSDocType` to support new `JSDocTypeFunction` 16 | - ✔ **[Added]** Improve type infering from assigned value. Currently support simple infering: `array`, `object`, `function`. 17 | - 🛠 **[Fixed]** Fix the [Issue #67](https://github.com/alexprey/sveltedoc-parser/issues/67), [Issue #69](https://github.com/alexprey/sveltedoc-parser/issues/69): specifier comments are not parsed properly; Thanks to @ekhaled 18 | - 🛠 **[Fixed]** Fix the [Issue #72](https://github.com/alexprey/sveltedoc-parser/issues/72): Module context scripts look for the wrong attribute 19 | - 🛠 **[Fixed]** Fix the [Issue #83](https://github.com/alexprey/sveltedoc-parser/issues/83): Default value and keywords of exported aliases not merged. 20 | 21 | ## [4.1.0] 19.02.2021 22 | 23 | - 🎉 **[Misc]** Update the ReadMe by [@soft-decay](https://github.com/soft-decay). 24 | - ✔ **[Added]** Implement support of imported types parsing, f.ex. `@type {import('../typings.d.ts').ExternalTypeClass}`. In order to do this, new field `importPath` introduced to `JSDocType`, in the name property now it returns imported class name, f.ex.: `ExternalTypeClass`. 25 | - 🛠 **[Fixed]** Complete fix of [Issue #1](https://github.com/alexprey/sveltedoc-parser/issues/1): Support parsing event names from top-level constant objects with accessing to their properties by naming strings. Introduce the new issue [Issue #48](https://github.com/alexprey/sveltedoc-parser/issues/48) about supporting parse of event names by external references. 26 | - 🛠 **[Fixed]** Fix the [Issue #47](https://github.com/alexprey/sveltedoc-parser/issues/48), now all comments in markup are parsed correctly and attached to required items in document. Support JSDoc comment markup parsing in all places where comment can be used. 27 | - 🛠 **[Fixed]** Fix the [Issue #61](https://github.com/alexprey/sveltedoc-parser/issues/61), now slot parameter items enrich with all detailed information that was parsed from markup comment. 28 | - 🛠 **[Fixed]** Spec: add the module definition typings to `typings.d.ts` file. 29 | - 🛠 **[Fixed]** Fix some edge-cases in script parsing logic. 30 | - 🛠 **[Tech]** Refactor internal parser logic to make it easy to introduce new features, moves forward to TS support! ;) 31 | - 🔥 **[Breaking]** Spec: change the `SvelteSlotParameter` definition, to support `name`, `description`, `type` fields, instead of many not relevant fields that was inherited from `ISvelteItem` interface. 32 | - 🔥 **[Breaking]** Spec: change the `SvelteSlotItem` definition, to improve consistency: 33 | - Rename `parameters` property to `params` to be most likely the same as `SvelteMethodItem`. Old field still available until 5.* release. 34 | 35 | Thanks a lot [@soft-decay](https://github.com/soft-decay) for contributing in this release! 36 | 37 | ## [4.0.0] 25.01.2021 38 | 39 | - 🛠 **[Fixed]** Fix [Issue #42](https://github.com/alexprey/sveltedoc-parser/issues/42) 40 | - 🛠 **[Fixed]** Partially fixed [Issue #1](https://github.com/alexprey/sveltedoc-parser/issues/1). Now event name correcly parsed if it provided by top-level constant of the same file. Thanks for [@soft-decay](https://github.com/soft-decay) 41 | - ✔ **[Added]** Support to complete parsing of component method arguments [Issue #39](https://github.com/alexprey/sveltedoc-parser/issues/39). Thanks for [@soft-decay](https://github.com/soft-decay) 42 | - ✔ **[Added]** Support to parse return types and description for methods in component [Issue #37](https://github.com/alexprey/sveltedoc-parser/issues/37). Thanks for [@soft-decay](https://github.com/soft-decay) 43 | - ✔ **[Added]** Options validation, thanks for [@soft-decay](https://github.com/soft-decay) 44 | - 🔥 **[Breaking]** API rework for component methods description: 45 | - `args` property was renamed to `params`; 46 | - Change the structure of `return` item for methods: 47 | - `desc` property was renamed to `description`; 48 | - `type` property now contains the `JSDocType` object, instead of `string` type with text representation of type. This can be gets from `text` property of `JSDocType` object; 49 | - [Svelte2]: method arguments was presented with plain array with names, now that replaced with objects of `SvelteMethodParamItem` type; 50 | - 🔥 **[Breaking]** Cleanup depricated code: 51 | - `loc` property was removed, please use `locations` instead, if you late with upgrade; 52 | - `value` property of `SvelteComponentItem` was removed, please use `importPath` instead 53 | 54 | ## [3.0.5] 28.11.2020 55 | 56 | - 🛠 **[Fixed]** Fix [Issue #35](https://github.com/alexprey/sveltedoc-parser/issues/35): Object literals not supported in @type keyword. Thanks for @soft-decay 57 | 58 | ## [3.0.4] 25.08.2020 59 | 60 | - 🛠 **[Fixed]** Fix [issue #5](https://github.com/alexprey/sveltedoc-parser/issues/5) (slots items have a private access level by default) 61 | 62 | ## [3.0.3] 25.08.2020 63 | 64 | - 🛠 **[Fixed]** Fix [issue #28](https://github.com/alexprey/sveltedoc-parser/issues/28) (Inline event handlers in markup cause errors when used without quotes) 65 | - Change dependency from `htmlparser2` to [htmlparser2-svelte](https://www.npmjs.com/package/htmlparser2-svelte) special fork of [htmlparser2](https://www.npmjs.com/package/htmlparser2) to handle Svelte specific cases to parse markup 66 | 67 | ## [3.0.2] 24.08.2020 68 | 69 | - 🛠 **[Fixed]** Fix issue #6 (Build a correct component name from a file name) 70 | ``` 71 | round.button.svelte -> RoundButton 72 | ``` 73 | - 🛠 **[Fixed]** Fix [issue #27](https://github.com/alexprey/sveltedoc-parser/issues/27) (Events is not exposed from exported functions and arrow functions) 74 | - 🛠 **[Fixed]** Fix [issue #31](https://github.com/alexprey/sveltedoc-parser/issues/31) (Propogated events in markup should be parsed even it was before handled) 75 | - 🛠 **[Fixed]** Fix [issue #32](https://github.com/alexprey/sveltedoc-parser/issues/32) (Event is not registered when dispatched from functions used as a parameters of another functions) 76 | 77 | ## [3.0.1] 17.08.2020 78 | 79 | - [Fixed] Solve issue #26, support `export { variables as var }` statement. 80 | - [Added] now interface `SvelteDataItem` provides a new property `localName` with information about internal name of component property. 81 | 82 | ## [3.0.0] 08.08.2020 83 | 84 | - [Fixed] Solve vulnerability issues: 85 | - Update `espree` to `7.2.0` 86 | - Update `htmlparser2` to `3.9.2` 87 | - Add dependency to `eslint` to fix issues after upgrading to new versions 88 | - [Breaking] Increase requirement of Node.js to `10.0.0`, Node.js v8 now is not supported, this is related with security isssues above. Please let me know if it still required. 89 | 90 | ## [2.3.4] 10.12.2019 91 | 92 | - [Fixed] Now `keywords` feature correctly supported. 93 | 94 | Thanks to [hontas](https://github.com/hontas) for following changes: 95 | 96 | - [Fixed] Svelte V3: Fix parsing of types for data items, defined by `@type` keyword. 97 | 98 | ## [2.3.3] 05.12.2019 99 | 100 | Thanks to [hontas](https://github.com/hontas) for following changes: 101 | 102 | - [Added] Svelte V3: Implement component documentation parsing provided by top level comment in HTML markup or in the JS section, marked with `@component` JSDoc attribute. 103 | 104 | ## [2.3.2] 02.12.2019 105 | 106 | Thanks to [Hostas](https://github.com/hontas) for following fixes: 107 | 108 | - [Fixed] Svelte V3: Improve type parsing for properties with default values. 109 | - [Fixed] Svelte V3: In some cases `type` property was setup with wrong structure and data, now it fixed. 110 | 111 | ## [2.3.1] 25.11.2019 112 | 113 | - [Fixed] Svelte V3: Fix parsing issues when anonymous functions are used in event handlers at markup (Issue #18) 114 | 115 | ## [2.3.0] 02.10.2019 116 | 117 | - [Added] Svelte V3: Implement support of script element locations 118 | - [Fixed] Svelte V3: Fix parsing when component have multiple ` 22 | 23 |
24 |
25 | 26 | 27 |
28 | {#if closable} 29 |
30 | 34 | 35 |
36 | {/if} 37 |
-------------------------------------------------------------------------------- /examples/Button.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 3, 3 | "name": "Button", 4 | "data": [ 5 | { 6 | "keywords": [ 7 | { 8 | "name": "type", 9 | "description": "{string}" 10 | } 11 | ], 12 | "visibility": "public", 13 | "description": "The text content of the button.", 14 | "name": "text", 15 | "kind": "let", 16 | "static": false, 17 | "readonly": false, 18 | "type": { 19 | "kind": "type", 20 | "text": "string", 21 | "type": "string" 22 | }, 23 | "defaultValue": "" 24 | }, 25 | { 26 | "keywords": [ 27 | { 28 | "name": "type", 29 | "description": "{'default'|'primary'|'danger'|'success'}" 30 | } 31 | ], 32 | "visibility": "public", 33 | "description": "The style type of the button.", 34 | "name": "type", 35 | "kind": "let", 36 | "static": false, 37 | "readonly": false, 38 | "type": { 39 | "kind": "union", 40 | "text": "'default'|'primary'|'danger'|'success'", 41 | "type": [ 42 | { 43 | "kind": "const", 44 | "text": "'default'", 45 | "type": "string", 46 | "value": "default" 47 | }, 48 | { 49 | "kind": "const", 50 | "text": "'primary'", 51 | "type": "string", 52 | "value": "primary" 53 | }, 54 | { 55 | "kind": "const", 56 | "text": "'danger'", 57 | "type": "string", 58 | "value": "danger" 59 | }, 60 | { 61 | "kind": "const", 62 | "text": "'success'", 63 | "type": "string", 64 | "value": "success" 65 | } 66 | ] 67 | }, 68 | "defaultValue": "default" 69 | }, 70 | { 71 | "keywords": [ 72 | { 73 | "name": "type", 74 | "description": "{'small'|'normal'|'big'}" 75 | } 76 | ], 77 | "visibility": "public", 78 | "description": "The size of the button.", 79 | "name": "size", 80 | "kind": "let", 81 | "static": false, 82 | "readonly": false, 83 | "type": { 84 | "kind": "union", 85 | "text": "'small'|'normal'|'big'", 86 | "type": [ 87 | { 88 | "kind": "const", 89 | "text": "'small'", 90 | "type": "string", 91 | "value": "small" 92 | }, 93 | { 94 | "kind": "const", 95 | "text": "'normal'", 96 | "type": "string", 97 | "value": "normal" 98 | }, 99 | { 100 | "kind": "const", 101 | "text": "'big'", 102 | "type": "string", 103 | "value": "big" 104 | } 105 | ] 106 | }, 107 | "defaultValue": "normal" 108 | }, 109 | { 110 | "keywords": [ 111 | { 112 | "name": "type", 113 | "description": "{{ rtl: boolean, lang: string }}" 114 | } 115 | ], 116 | "visibility": "public", 117 | "description": "Additional options for the button.", 118 | "name": "options", 119 | "kind": "let", 120 | "static": false, 121 | "readonly": false, 122 | "type": { 123 | "kind": "type", 124 | "text": "{ rtl: boolean, lang: string }", 125 | "type": "{ rtl: boolean, lang: string }" 126 | } 127 | } 128 | ], 129 | "computed": [], 130 | "methods": [ 131 | { 132 | "keywords": [ 133 | { 134 | "name": "param", 135 | "description": "{string} [question=Why?] a question about life, the universe, everything" 136 | }, 137 | { 138 | "name": "returns", 139 | "description": "{number} the answer to all your questions" 140 | } 141 | ], 142 | "visibility": "public", 143 | "description": "Computes the answer to your question.", 144 | "name": "computeAnswer", 145 | "params": [ 146 | { 147 | "type": { 148 | "kind": "type", 149 | "text": "string", 150 | "type": "string" 151 | }, 152 | "name": "question", 153 | "description": "a question about life, the universe, everything", 154 | "optional": true, 155 | "defaultValue": "Why?" 156 | } 157 | ], 158 | "return": { 159 | "type": { 160 | "kind": "type", 161 | "text": "number", 162 | "type": "number" 163 | }, 164 | "description": "the answer to all your questions" 165 | }, 166 | "static": false 167 | } 168 | ], 169 | "components": [], 170 | "description": "The simple component button.", 171 | "keywords": [ 172 | { 173 | "name": "component", 174 | "description": "Button" 175 | }, 176 | { 177 | "name": "example", 178 | "description": " -------------------------------------------------------------------------------- /examples/SlotList.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 3, 3 | "name": "SlotList", 4 | "data": [ 5 | { 6 | "visibility": "public", 7 | "description": null, 8 | "keywords": [], 9 | "name": "items", 10 | "kind": "let", 11 | "static": false, 12 | "readonly": false, 13 | "type": { 14 | "kind": "type", 15 | "text": "any", 16 | "type": "any" 17 | } 18 | } 19 | ], 20 | "computed": [], 21 | "methods": [], 22 | "components": [], 23 | "description": null, 24 | "keywords": [], 25 | "events": [], 26 | "slots": [ 27 | { 28 | "keywords": [ 29 | { 30 | "name": "param", 31 | "description": "{GenericListItem} prop The item of the list that should be rendered." 32 | } 33 | ], 34 | "visibility": "public", 35 | "description": "Render the one item markup based on data passed in slot property.", 36 | "name": "default", 37 | "params": [ 38 | { 39 | "type": { 40 | "kind": "type", 41 | "text": "GenericListItem", 42 | "type": "GenericListItem" 43 | }, 44 | "name": "prop", 45 | "description": "The item of the list that should be rendered.", 46 | "optional": false 47 | } 48 | ], 49 | "parameters": [ 50 | { 51 | "type": { 52 | "kind": "type", 53 | "text": "GenericListItem", 54 | "type": "GenericListItem" 55 | }, 56 | "name": "prop", 57 | "description": "The item of the list that should be rendered.", 58 | "optional": false 59 | } 60 | ] 61 | }, 62 | { 63 | "keywords": [], 64 | "visibility": "public", 65 | "description": "", 66 | "name": "empty" 67 | } 68 | ], 69 | "refs": [] 70 | } -------------------------------------------------------------------------------- /examples/SlotList.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 |
6 | {#each items as item} 7 | 11 | 12 | {:else} 13 | 14 | {/each} 15 |
-------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const { loadFileStructureFromOptions } = require('./lib/helpers'); 2 | const { validate, retrieveFileOptions } = require('./lib/options'); 3 | const SvelteVersionDetector = require('./lib/detector'); 4 | const { SVELTE_VERSION_2, SVELTE_VERSION_3 } = require('./lib/detector'); 5 | 6 | /** 7 | * @typedef {import("./typings").SvelteParserOptions} SvelteParserOptions 8 | */ 9 | 10 | function buildSvelte2Parser(options) { 11 | const Parser = require('./lib/parser'); 12 | 13 | // Convert structure object to old version source options 14 | const { scripts, styles, template } = options.structure; 15 | 16 | const hasScript = !!scripts && scripts.length > 0; 17 | const hasStyle = !!styles && styles.length > 0; 18 | 19 | options.source = { 20 | template: template, 21 | script: hasScript ? scripts[0].content : '', 22 | scriptOffset: hasScript ? scripts[0].offset : 0, 23 | style: hasStyle ? styles[0].content : '', 24 | styleOffset: hasStyle ? styles[0].offset : 0, 25 | }; 26 | 27 | return new Parser(options); 28 | } 29 | 30 | function buildSvelte3Parser(options) { 31 | const Parser = require('./lib/v3/parser'); 32 | 33 | return new Parser(options); 34 | } 35 | 36 | function buildSvelteParser(options, version) { 37 | if (version === SvelteVersionDetector.SVELTE_VERSION_3) { 38 | return buildSvelte3Parser(options); 39 | } 40 | 41 | if (version === SvelteVersionDetector.SVELTE_VERSION_2) { 42 | return buildSvelte2Parser(options); 43 | } 44 | 45 | if (version) { 46 | throw new Error(`Svelte V${version} is not supported`); 47 | } 48 | 49 | throw new Error('Undefined Svelte version is not supported, you should specify default version in options'); 50 | } 51 | 52 | function getEventName(feature) { 53 | return feature.endsWith('s') 54 | ? feature.substring(0, feature.length - 1) 55 | : feature; 56 | } 57 | 58 | function convertVisibilityToLevel(visibility) { 59 | switch (visibility) { 60 | case 'public': 61 | return 3; 62 | case 'protected': 63 | return 2; 64 | case 'private': 65 | return 1; 66 | } 67 | 68 | return 0; 69 | } 70 | 71 | function mergeItems(itemType, currentItem, newItem, ignoreLocations) { 72 | if (convertVisibilityToLevel(currentItem.visibility) < convertVisibilityToLevel(newItem.visibility)) { 73 | currentItem.visibility = newItem.visibility; 74 | } 75 | 76 | if (!currentItem.description && newItem.description) { 77 | currentItem.description = newItem.description; 78 | } 79 | 80 | if (!currentItem.defaultValue && typeof newItem.defaultValue !== 'undefined') { 81 | currentItem.defaultValue = newItem.defaultValue; 82 | } 83 | 84 | if (!currentItem.type || currentItem.type.type === 'any') { 85 | if (newItem.type && newItem.type.type !== 'any') { 86 | currentItem.type = newItem.type; 87 | } 88 | } 89 | 90 | if ((!currentItem.keywords || currentItem.keywords.length === 0) && newItem.keywords) { 91 | currentItem.keywords = newItem.keywords; 92 | } 93 | 94 | if (!ignoreLocations) { 95 | if (newItem.locations && newItem.locations.length > 0) { 96 | if (currentItem.locations) { 97 | currentItem.locations.push(...newItem.locations); 98 | } else { 99 | currentItem.locations = [...newItem.locations]; 100 | } 101 | } 102 | } 103 | 104 | if (itemType === 'data') { 105 | if (newItem.bind && newItem.bind.length > 0) { 106 | if (currentItem.bind) { 107 | currentItem.bind.push(...newItem.bind); 108 | } else { 109 | currentItem.bind = [...newItem.bind]; 110 | } 111 | } 112 | } 113 | 114 | return currentItem; 115 | } 116 | 117 | function subscribeOnParserEvents(parser, ignoredVisibilities, version, resolve, reject) { 118 | const component = { 119 | version: version 120 | }; 121 | 122 | parser.features.forEach((feature) => { 123 | switch (feature) { 124 | case 'name': 125 | case 'description': 126 | component[feature] = null; 127 | parser.on(feature, (value) => (component[feature] = value)); 128 | break; 129 | 130 | case 'keywords': 131 | component[feature] = []; 132 | parser.on(feature, (value) => (component[feature] = value)); 133 | break; 134 | 135 | default: { 136 | component[feature] = []; 137 | 138 | const eventName = getEventName(feature); 139 | 140 | parser.on(eventName, (value) => { 141 | const itemIndex = component[feature].findIndex(item => item.name === value.name); 142 | 143 | if (value.localName) { 144 | const localItem = component[feature].find(item => item.name === value.localName); 145 | 146 | if (localItem) { 147 | value = mergeItems(feature, value, localItem, true); 148 | } 149 | } 150 | 151 | if (itemIndex < 0) { 152 | component[feature].push(value); 153 | } else { 154 | const currentItem = component[feature][itemIndex]; 155 | 156 | component[feature][itemIndex] = mergeItems(feature, currentItem, value); 157 | } 158 | }); 159 | } 160 | } 161 | }); 162 | 163 | parser.on('end', () => { 164 | parser.features.forEach((feature) => { 165 | if (component[feature] instanceof Array) { 166 | component[feature] = component[feature].filter((item) => { 167 | return !ignoredVisibilities.includes(item.visibility); 168 | }); 169 | } 170 | }); 171 | 172 | resolve(component); 173 | }); 174 | 175 | parser.on('failure', (error) => { 176 | reject(error); 177 | }); 178 | } 179 | 180 | /** 181 | * Main parse function. 182 | * @param {SvelteParserOptions} options 183 | * @return {Promise} 184 | * @example 185 | * const { parse } = require('sveltedoc-parser'); 186 | * // basic usage only requires 'filename' to be set. 187 | * const doc = await parse({ 188 | * filename: 'main.svelte', 189 | * encoding: 'ascii', 190 | * features: ['data', 'computed', 'methods'], 191 | * ignoredVisibilities: ['private'], 192 | * includeSourceLocations: true, 193 | * version: 3 194 | * }); 195 | */ 196 | module.exports.parse = (options) => new Promise((resolve, reject) => { 197 | try { 198 | validate(options); 199 | 200 | const fileOptions = retrieveFileOptions(options); 201 | 202 | options.structure = loadFileStructureFromOptions(fileOptions); 203 | 204 | const version = options.version || SvelteVersionDetector.detectVersionFromStructure(options.structure, options.defaultVersion); 205 | 206 | const parser = buildSvelteParser(options, version); 207 | 208 | subscribeOnParserEvents(parser, options.ignoredVisibilities, version, resolve, reject); 209 | 210 | parser.walk(); 211 | } catch (error) { 212 | reject(error); 213 | } 214 | }); 215 | 216 | /** 217 | * @param {SvelteParserOptions} options 218 | */ 219 | module.exports.detectVersion = (options) => { 220 | validate(options); 221 | 222 | return SvelteVersionDetector.detectVersionFromOptions(options); 223 | }; 224 | 225 | module.exports.SVELTE_VERSION_2 = SVELTE_VERSION_2; 226 | module.exports.SVELTE_VERSION_3 = SVELTE_VERSION_3; 227 | -------------------------------------------------------------------------------- /lib/detector.js: -------------------------------------------------------------------------------- 1 | const { loadFileStructureFromOptions: loadFileContentFromOptions } = require('./helpers'); 2 | 3 | const SVELTE_VERSION_2 = 2; 4 | const SVELTE_VERSION_3 = 3; 5 | 6 | function isElseIfWithoutSpaceInTemplate(template) { 7 | return /\{:elseif\s/gi.test(template); 8 | } 9 | 10 | function isElseIfWithSpaceInTemplate(template) { 11 | return /\{:else\s+if\s/gi.test(template); 12 | } 13 | 14 | function isRefsIsUsedInTemplate(template) { 15 | return /\sref:[^\s]+\s/gi.test(template); 16 | } 17 | 18 | function isLetUsedForSlotInTemplate(template) { 19 | return /\slet:[^=]+=/gi.test(template); 20 | } 21 | 22 | function isContextUsedForScriptBlock(scriptBlock) { 23 | return /\scontext=/gi.test(scriptBlock.attributes); 24 | } 25 | 26 | function isExportDefaultUsedInBlock(scriptBlock) { 27 | return /\bexport\s+default\s+\{/gi.test(scriptBlock.content); 28 | } 29 | 30 | function detectVersionFromOptions(options) { 31 | const structure = loadFileContentFromOptions(options); 32 | 33 | return detectVersionFromStructure(structure, options.defaultVersion); 34 | } 35 | 36 | function detectVersionFromStructure(structure, defaultVersion) { 37 | if (structure.template) { 38 | // The `ref:` attribute is valid for Svelte 2 only 39 | if (isRefsIsUsedInTemplate(structure.template)) { 40 | return SVELTE_VERSION_2; 41 | } 42 | 43 | // The `{:elseif ...}` statement is possible only in Svelte 2 44 | if (isElseIfWithoutSpaceInTemplate(structure.template)) { 45 | return SVELTE_VERSION_2; 46 | } 47 | 48 | // The `{:else if ...}` statement is valid for Svelte 3 49 | if (isElseIfWithSpaceInTemplate(structure.template)) { 50 | return SVELTE_VERSION_3; 51 | } 52 | 53 | // The `let:item=...` syntax is valid for Svelte 3 54 | if (isLetUsedForSlotInTemplate(structure.template)) { 55 | return SVELTE_VERSION_3; 56 | } 57 | } 58 | 59 | if (structure.scripts) { 60 | // In Svelte 3 is possible to use more than one script blocks 61 | if (structure.scripts.length > 1) { 62 | return SVELTE_VERSION_3; 63 | } 64 | 65 | // In Svelte 3 is possible to define context attribute for script blocks 66 | if (structure.scripts.some(scriptBlock => isContextUsedForScriptBlock(scriptBlock))) { 67 | return SVELTE_VERSION_3; 68 | } 69 | 70 | // Export default statement can be used only in Svelte 2 71 | if (structure.scripts.some(scriptBlock => isExportDefaultUsedInBlock(scriptBlock))) { 72 | return SVELTE_VERSION_2; 73 | } 74 | } 75 | 76 | // Return the default version if provided 77 | if (defaultVersion) { 78 | return defaultVersion; 79 | } 80 | 81 | // If can't recognize version, return undefined 82 | return undefined; 83 | } 84 | 85 | module.exports = { 86 | SVELTE_VERSION_2, 87 | SVELTE_VERSION_3, 88 | detectVersionFromOptions, 89 | detectVersionFromStructure 90 | }; 91 | -------------------------------------------------------------------------------- /lib/helpers.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | 4 | /** 5 | * @typedef {import('../typings').SvelteParserOptions} SvelteParserOptions 6 | * @typedef {'fileContent' | 'filename' | 'encoding'} FileOptionKeys 7 | * @typedef {{content?: string; template?: string, scripts?: HtmlBlock[], styles?: HtmlBlock[] }} FileStructure 8 | * @typedef {Pick & { structure?: FileStructure }} FileOptions 9 | */ 10 | 11 | /** 12 | * @typedef {{ offset: number, outerPosition: { start: number, end: number }, content: string, attributes: string }} HtmlBlock 13 | */ 14 | 15 | /** 16 | * 17 | * @param {FileOptions} options 18 | */ 19 | function loadFileStructureFromOptions(options) { 20 | if (options.structure) { 21 | return options.structure; 22 | } 23 | 24 | if (options.fileContent) { 25 | return parseFileStructureFromContent(options.fileContent); 26 | } 27 | 28 | if (options.filename) { 29 | if (path.extname(options.filename) === '.js') { 30 | return { 31 | template: '', 32 | scripts: [{ 33 | content: fs.readFileSync(options.filename, options.encoding), 34 | attributes: '', 35 | offset: 0 36 | }], 37 | styles: [] 38 | }; 39 | } else { 40 | const buffer = fs.readFileSync(options.filename, options.encoding); 41 | 42 | return parseFileStructureFromContent(buffer.toString()); 43 | } 44 | } 45 | 46 | // With options validation, this should never happen 47 | throw new Error('Internal Error: Cannot load FileStructure because input is missing.'); 48 | } 49 | 50 | /** 51 | * 52 | * @param {string} content 53 | * @param {string} blockName 54 | * @param {number} searchStartIndex 55 | * @returns {{ block: HtmlBlock | null }} 56 | */ 57 | function extractHtmlBlock(content, blockName, searchStartIndex) { 58 | const blockOuterStartIndex = content.indexOf(`<${blockName}`, searchStartIndex); 59 | 60 | if (blockOuterStartIndex >= 0) { 61 | const blockInnerEndIndex = content.indexOf(``, blockOuterStartIndex + blockName.length + 1); 62 | 63 | if (blockInnerEndIndex >= 0) { 64 | const openTagEndIndex = content.indexOf('>', blockOuterStartIndex + blockName.length); 65 | 66 | const attributes = content.substr(blockOuterStartIndex + blockName.length + 1, openTagEndIndex - blockOuterStartIndex - blockName.length - 1); 67 | const innerBlockContent = content.substr(openTagEndIndex + 1, blockInnerEndIndex - openTagEndIndex - 1); 68 | const offset = openTagEndIndex + 1; 69 | 70 | const blockOuterEndIndex = blockInnerEndIndex + blockName.length + 3; 71 | 72 | return { 73 | block: { 74 | offset: offset, 75 | outerPosition: { 76 | start: blockOuterStartIndex, 77 | end: blockOuterEndIndex 78 | }, 79 | content: innerBlockContent, 80 | attributes: attributes 81 | } 82 | }; 83 | } 84 | } 85 | 86 | return { 87 | block: null 88 | }; 89 | } 90 | 91 | /** 92 | * 93 | * @param {string} content 94 | * @param {string} blockName 95 | * @returns {{ blocks: HtmlBlock[] }} 96 | */ 97 | function extractAllHtmlBlocks(content, blockName) { 98 | /** @type {HtmlBlock[]} */ 99 | const blocks = []; 100 | 101 | let searchResult = extractHtmlBlock(content, blockName); 102 | 103 | while (searchResult.block) { 104 | blocks.push(searchResult.block); 105 | 106 | const searchStartIndex = searchResult.block.outerPosition.end; 107 | 108 | searchResult = extractHtmlBlock(content, blockName, searchStartIndex); 109 | } 110 | 111 | return { 112 | blocks: blocks 113 | }; 114 | } 115 | 116 | /** 117 | * 118 | * @param {string} fileContent 119 | * @returns {FileStructure} 120 | */ 121 | function parseFileStructureFromContent(fileContent) { 122 | const scriptBlocksSearchResult = extractAllHtmlBlocks(fileContent, 'script'); 123 | const styleBlocksSearchResult = extractAllHtmlBlocks(fileContent, 'style'); 124 | 125 | return { 126 | content: fileContent, 127 | template: fileContent, 128 | scripts: scriptBlocksSearchResult.blocks, 129 | styles: styleBlocksSearchResult.blocks 130 | }; 131 | } 132 | 133 | module.exports = { 134 | loadFileStructureFromOptions, 135 | extractHtmlBlock, 136 | extractAllHtmlBlocks, 137 | parseFileStructureFromContent 138 | }; 139 | -------------------------------------------------------------------------------- /lib/jsdoc.js: -------------------------------------------------------------------------------- 1 | 2 | const PARAM_NAME = '[a-z0-9$\\.\\[\\]_]+'; 3 | 4 | const TYPE = '\\{(.*)\\}'; 5 | const TYPE_DESC = '-?\\s*(.*)'; 6 | const PARAM_TYPE = '\\{((?:\\.\\.\\.)?[^\\}]*)=?\\}'; 7 | 8 | const TYPE_RE = new RegExp(`^\\s*${TYPE}`, 'i'); 9 | const PARAM_RE = new RegExp(`^\\s*(?:${PARAM_TYPE}\\s+)?(?:\\[\\s*(${PARAM_NAME})\\s*(?:=\\s*([^\\]]+))?\\]|(${PARAM_NAME}))(?:\\s+(?:\\-\\s+)?(.*))?`, 'i'); 10 | const RETURN_RE = new RegExp(`^\\s*(${TYPE})?(\\s*${TYPE_DESC})?`, 'i'); 11 | const TYPE_IMPORT_RE = new RegExp('^import\\s*\\(\\s*(?:\'([^\']*)\'|"([^"]*)")\\s*\\)\\.(.+)$'); 12 | 13 | const DEFAULT_TYPE = 'any'; 14 | 15 | function getDefaultJSDocType() { 16 | return { 17 | kind: 'type', 18 | text: '*', 19 | type: DEFAULT_TYPE 20 | }; 21 | } 22 | 23 | function parseType(type, param) { 24 | if (type.indexOf('|') > -1) { 25 | param.type = type.split('|'); 26 | } else if (type.startsWith('...')) { 27 | param.type = type.substring(3); 28 | param.repeated = true; 29 | } else if (type === '*') { 30 | param.type = DEFAULT_TYPE; 31 | } else { 32 | param.type = type; 33 | } 34 | } 35 | 36 | function parseJSDocType(typeValue) { 37 | typeValue = typeValue.trim(); 38 | 39 | if (typeValue.indexOf('|') > -1) { 40 | if (typeValue.startsWith('(') && typeValue.endsWith(')')) { 41 | typeValue = typeValue.substring(1, typeValue.length - 1).trim(); 42 | } 43 | 44 | const types = typeValue.split('|'); 45 | 46 | return { 47 | kind: 'union', 48 | text: typeValue, 49 | type: types 50 | .map(type => parseJSDocType(type)) 51 | .filter(type => type != null) 52 | }; 53 | } 54 | 55 | if (typeValue.startsWith('\'') && typeValue.endsWith('\'')) { 56 | return { 57 | kind: 'const', 58 | text: typeValue, 59 | type: 'string', 60 | value: typeValue.substr(1, typeValue.length - 2) 61 | }; 62 | } 63 | 64 | if (typeValue.startsWith('import(')) { 65 | const match = TYPE_IMPORT_RE.exec(typeValue); 66 | 67 | if (match) { 68 | const importPath = match[1] || match[2]; 69 | const importedType = match[3]; 70 | 71 | return { 72 | kind: 'type', 73 | text: importedType, 74 | type: importedType, 75 | importPath: importPath 76 | }; 77 | } 78 | } 79 | 80 | if (typeValue === '*') { 81 | return { 82 | kind: 'type', 83 | text: typeValue, 84 | type: DEFAULT_TYPE 85 | }; 86 | } 87 | 88 | return { 89 | kind: 'type', 90 | text: typeValue, 91 | type: typeValue 92 | }; 93 | } 94 | 95 | function parseJSTypeFromValueNode(valueNode) { 96 | if (valueNode == null) { 97 | return null; 98 | } 99 | 100 | if (valueNode.type === 'ArrayExpression') { 101 | return { 102 | kind: 'type', 103 | text: 'Array', 104 | type: 'Array' 105 | }; 106 | } 107 | 108 | if (typeof (valueNode) === 'object') { 109 | return { 110 | kind: 'type', 111 | text: 'any', 112 | type: 'any' 113 | }; 114 | } 115 | 116 | return { 117 | kind: 'type', 118 | text: typeof (valueNode), 119 | type: typeof (valueNode) 120 | }; 121 | } 122 | 123 | function parseTypeKeyword(text) { 124 | const match = TYPE_RE.exec(text); 125 | 126 | if (match) { 127 | const typeValue = match[1]; 128 | 129 | if (typeValue) { 130 | return parseJSDocType(typeValue); 131 | } 132 | } 133 | 134 | return null; 135 | } 136 | 137 | function parseParamKeyword(text) { 138 | const param = { 139 | type: getDefaultJSDocType(), 140 | name: null, 141 | description: null, 142 | optional: false 143 | }; 144 | 145 | const match = PARAM_RE.exec(text); 146 | 147 | if (match) { 148 | if (match[1]) { 149 | let parameterType = match[1]; 150 | 151 | // Check if that repeated parameter, like `@param {...string} parameter` 152 | if (parameterType.startsWith('...')) { 153 | param.repeated = true; 154 | parameterType = parameterType.substr(3); 155 | } 156 | 157 | // Check Google Closure Compiler syntax for optional parameters, like `@param {string=} parameter` 158 | if (parameterType.endsWith('=')) { 159 | param.optional = true; 160 | parameterType = parameterType.substr(0, parameterType.length - 1); 161 | } 162 | 163 | param.type = parseJSDocType(parameterType); 164 | } 165 | 166 | // Optional parameter name 167 | if (match[2]) { 168 | param.name = match[2].trim(); 169 | param.optional = true; 170 | 171 | if (match[3]) { 172 | param.defaultValue = match[3].trim(); 173 | } 174 | } 175 | 176 | // Required parameter name 177 | if (match[4]) { 178 | param.name = match[4].trim(); 179 | } 180 | 181 | // Description 182 | if (match[5]) { 183 | param.description = match[5].trim(); 184 | } 185 | } 186 | 187 | return param; 188 | } 189 | 190 | function parseReturnKeyword(text) { 191 | const output = { 192 | type: getDefaultJSDocType(), 193 | description: null 194 | }; 195 | 196 | const matches = RETURN_RE.exec(text); 197 | const typeMatch = matches[2]; // type, excluding curly braces 198 | 199 | if (typeMatch) { 200 | output.type = parseJSDocType(typeMatch); 201 | } 202 | 203 | const descriptionMatch = matches[4]; // description, excluding prefix 204 | 205 | if (descriptionMatch) { 206 | output.description = descriptionMatch.trim(); 207 | } 208 | 209 | return output; 210 | } 211 | 212 | /** 213 | * @param {string} comment 214 | * @return {string} 215 | */ 216 | const convertToJsDocComment = (comment) => { 217 | if (!comment) { 218 | return ''; 219 | } 220 | 221 | comment = comment.trim(); 222 | 223 | // Comment content already is JSDoc format 224 | if (comment.startsWith('/**') && comment.endsWith('*/')) { 225 | return comment; 226 | } 227 | 228 | const lines = comment.split('\n'); 229 | 230 | return lines.map((line, lineNumber) => { 231 | line = line.trim(); 232 | 233 | if (lineNumber === 0) { 234 | if (!line.startsWith('/**')) { 235 | line = `/**\n * ${line}`; 236 | } 237 | } 238 | 239 | if (lineNumber === lines.length - 1) { 240 | if (!line.endsWith('*/')) { 241 | line = `${line}\n */`; 242 | } 243 | } 244 | 245 | return line.trimEnd(); 246 | }).join('\n * '); 247 | }; 248 | 249 | module.exports = { 250 | parseType, 251 | parseParamKeyword, 252 | parseReturnKeyword, 253 | parseTypeKeyword, 254 | parseJSTypeFromValueNode, 255 | 256 | convertToJsDocComment, 257 | getDefaultJSDocType, 258 | 259 | DEFAULT_TYPE 260 | }; 261 | -------------------------------------------------------------------------------- /lib/options.js: -------------------------------------------------------------------------------- 1 | const { isString, stringifyArray, isVisibilitySupported, VISIBILITIES } = require('./utils'); 2 | 3 | /** 4 | * @typedef {import("../typings").SvelteParserOptions} SvelteParserOptions 5 | * @typedef {import("../typings").JSVisibilityScope} JSVisibilityScope 6 | */ 7 | 8 | /** @type {BufferEncoding[]} */ 9 | const ENCODINGS = [ 10 | 'ascii', 11 | 'utf8', 12 | 'utf-8', 13 | 'utf16le', 14 | 'ucs2', 15 | 'ucs-2', 16 | 'base64', 17 | 'latin1', 18 | 'binary', 19 | 'hex' 20 | ]; 21 | 22 | const ERROR_ENCODING_FORMAT = 'Expected options.encoding to be a string. '; 23 | const ERROR_VISIBILITIES_FORMAT = 'Expected options.ignoredVisibilities to be an array of strings. '; 24 | const INFO_ENCODING_SUPPORTED = `Supported encodings: ${stringifyArray(ENCODINGS)}.`; 25 | const INFO_VISIBILITIES_SUPPORTED = `Supported visibilities: ${stringifyArray(VISIBILITIES)}.`; 26 | 27 | function getUnsupportedEncodingString(enc) { 28 | return `encoding ${stringifyArray([enc])} not supported. ` + 29 | INFO_ENCODING_SUPPORTED; 30 | } 31 | 32 | function getUnsupportedVisibilitiesString(arr) { 33 | return `Visibilities [${stringifyArray(arr)}] in ` + 34 | 'options.ignoredVisibilities are not supported. ' + 35 | INFO_VISIBILITIES_SUPPORTED; 36 | } 37 | 38 | const OptionsError = Object.freeze({ 39 | OptionsRequired: 'An options object is required.', 40 | InputRequired: 'One of options.filename or options.fileContent is required.', 41 | EncodingMissing: 'Internal Error: options.encoding is not set.', 42 | EncodingFormat: ERROR_ENCODING_FORMAT + INFO_ENCODING_SUPPORTED, 43 | EncodingNotSupported: (enc) => getUnsupportedEncodingString(enc), 44 | IgnoredVisibilitiesMissing: 'Internal Error: options.ignoredVisibilities is not set.', 45 | IgnoredVisibilitiesFormat: ERROR_VISIBILITIES_FORMAT + INFO_VISIBILITIES_SUPPORTED, 46 | IgnoredVisibilitiesNotSupported: (arr) => getUnsupportedVisibilitiesString(arr), 47 | IncludeSourceLocationsMissing: 'Internal Error: options.includeSourceLocationsMissing is not set.', 48 | IncludeSourceLocationsFormat: 'Expected options.includeSourceLocations to be a boolean.', 49 | }); 50 | 51 | /** @type {BufferEncoding} */ 52 | const DEFAULT_ENCODING = 'utf8'; 53 | 54 | /** @type {JSVisibilityScope[]} */ 55 | const DEFAULT_IGNORED_VISIBILITIES = ['protected', 'private']; 56 | 57 | /** @returns {SvelteParserOptions} */ 58 | function getDefaultOptions() { 59 | return { 60 | encoding: DEFAULT_ENCODING, 61 | ignoredVisibilities: [...DEFAULT_IGNORED_VISIBILITIES], 62 | includeSourceLocations: false, 63 | }; 64 | } 65 | 66 | function retrieveFileOptions(options) { 67 | return { 68 | structure: options.structure, 69 | fileContent: options.fileContent, 70 | filename: options.filename, 71 | encoding: options.encoding, 72 | }; 73 | } 74 | 75 | /** 76 | * Applies default values to options. 77 | * @param {SvelteParserOptions} options object to normalize (mutated) 78 | * @param {SvelteParserOptions} defaults default values to normalize 'options' 79 | */ 80 | function normalize(options, defaults) { 81 | Object.keys(defaults).forEach((optionKey) => { 82 | /** 83 | * If the key was not set by the user, apply default value. 84 | * This is better than checking for falsy values because it catches 85 | * use cases were a user tried to do something not intended with 86 | * an option (e.g. putting a value of 'false' or an empty string) 87 | */ 88 | if (!(optionKey in options)) { 89 | options[optionKey] = defaults[optionKey]; 90 | } 91 | }); 92 | } 93 | 94 | /** 95 | * @param {SvelteParserOptions} options 96 | * @throws an error if any options are invalid 97 | */ 98 | function validate(options) { 99 | if (!options) { 100 | throw new Error(OptionsError.OptionsRequired); 101 | } 102 | 103 | normalize(options, getDefaultOptions()); 104 | 105 | const hasFilename = 106 | ('filename' in options) && 107 | isString(options.filename) && 108 | options.filename.length > 0; 109 | 110 | // Don't check length for fileContent because it could be an empty file. 111 | const hasFileContent = 112 | ('fileContent' in options) && 113 | isString(options.fileContent); 114 | 115 | if (!hasFilename && !hasFileContent) { 116 | throw new Error(OptionsError.InputRequired); 117 | } 118 | 119 | if ('encoding' in options) { 120 | if (!isString(options.encoding)) { 121 | throw new Error(OptionsError.EncodingFormat); 122 | } 123 | 124 | if (!ENCODINGS.includes(options.encoding)) { 125 | throw new Error(OptionsError.EncodingNotSupported(options.encoding)); 126 | } 127 | } else { 128 | // Sanity check. At this point, 'encoding' must be set. 129 | throw new Error(OptionsError.EncodingMissing); 130 | } 131 | 132 | if ('ignoredVisibilities' in options) { 133 | if (!Array.isArray(options.ignoredVisibilities)) { 134 | throw new Error(OptionsError.IgnoredVisibilitiesFormat); 135 | } 136 | 137 | if (!options.ignoredVisibilities.every(isVisibilitySupported)) { 138 | const notSupported = options.ignoredVisibilities.filter( 139 | (iv) => !isVisibilitySupported(iv) 140 | ); 141 | 142 | throw new Error(OptionsError.IgnoredVisibilitiesNotSupported(notSupported)); 143 | } 144 | } else { 145 | // Sanity check. At this point, 'ignoredVisibilities' must be set. 146 | throw new Error(OptionsError.IgnoredVisibilitiesMissing); 147 | } 148 | 149 | if ('includeSourceLocations' in options) { 150 | if (typeof options.includeSourceLocations !== 'boolean') { 151 | throw new TypeError(OptionsError.IncludeSourceLocationsFormat); 152 | } 153 | } else { 154 | // Sanity check. At this point, 'includeSourceLocations' must be set. 155 | throw new Error(OptionsError.IncludeSourceLocationsMissing); 156 | } 157 | } 158 | 159 | const getSupportedFeaturesString = (supported) => `Supported features: ${stringifyArray(supported)}`; 160 | 161 | const getFeaturesEmptyString = (supported) => { 162 | return 'options.features must contain at least one feature. ' + 163 | getSupportedFeaturesString(supported); 164 | }; 165 | 166 | /** 167 | * @param {string[]} notSupported 168 | * @param {string[]} supported 169 | */ 170 | const getFeaturesNotSupportedString = (notSupported, supported) => { 171 | return `Features [${stringifyArray(notSupported)}] in ` + 172 | 'options.features are not supported by this Parser. ' + 173 | getSupportedFeaturesString(supported); 174 | }; 175 | 176 | const ParserError = { 177 | FeaturesMissing: 'Internal Error: options.features is not set.', 178 | FeaturesFormat: 'options.features must be an array', 179 | FeaturesEmpty: getFeaturesEmptyString, 180 | FeaturesNotSupported: getFeaturesNotSupportedString, 181 | }; 182 | 183 | /** 184 | * 185 | * @param {SvelteParserOptions} options 186 | * @param {string[]} supported 187 | * @throws if any validation fails for options.features 188 | */ 189 | function validateFeatures(options, supported) { 190 | if ('features' in options) { 191 | if (!Array.isArray(options.features)) { 192 | throw new TypeError(ParserError.FeaturesFormat); 193 | } 194 | 195 | if (options.features.length === 0) { 196 | throw new Error(ParserError.FeaturesEmpty(supported)); 197 | } 198 | 199 | const notSupported = options.features.filter((iv) => !supported.includes(iv)); 200 | 201 | if (notSupported.length > 0) { 202 | throw new Error(ParserError.FeaturesNotSupported(notSupported, supported)); 203 | } 204 | } else { 205 | throw new Error(ParserError.FeaturesMissing); 206 | } 207 | } 208 | 209 | /** 210 | * @link https://github.com/eslint/espree#options 211 | */ 212 | function getAstDefaultOptions() { 213 | return { 214 | /** attach range information to each node */ 215 | range: true, 216 | 217 | /** attach line/column location information to each node */ 218 | loc: true, 219 | 220 | /** create a top-level comments array containing all comments */ 221 | comment: true, 222 | 223 | /** create a top-level tokens array containing all tokens */ 224 | tokens: true, 225 | 226 | /** 227 | * Set to 3, 5 (default), 6, 7, 8, 9, 10, 11, or 12 to specify 228 | * the version of ECMAScript syntax you want to use. 229 | * 230 | * You can also set to 2015 (same as 6), 2016 (same as 7), 231 | * 2017 (same as 8), 2018 (same as 9), 2019 (same as 10), 232 | * 2020 (same as 11), or 2021 (same as 12) to use the year-based naming. 233 | */ 234 | ecmaVersion: 9, 235 | 236 | /** specify which type of script you're parsing ("script" or "module") */ 237 | sourceType: 'module', 238 | 239 | /** specify additional language features */ 240 | ecmaFeatures: {} 241 | }; 242 | } 243 | 244 | module.exports = { 245 | OptionsError, 246 | ParserError, 247 | normalize, 248 | validate, 249 | validateFeatures, 250 | retrieveFileOptions, 251 | getAstDefaultOptions, 252 | }; 253 | -------------------------------------------------------------------------------- /lib/v3/events.js: -------------------------------------------------------------------------------- 1 | const CommonEvent = Object.freeze({ 2 | /** 3 | * Emit the @see {SvelteDataItem} object. 4 | */ 5 | DATA: 'data', 6 | /** 7 | * Emit the @see {SvelteEventItem} object. 8 | */ 9 | EVENT: 'event', 10 | /** 11 | * Emit the global comment @see {IScopedCommentItem} object. 12 | */ 13 | GLOBAL_COMMENT: 'global-comment', 14 | }); 15 | 16 | const TemplateEvent = Object.freeze({ 17 | ...CommonEvent, 18 | NAME: 'name', 19 | REF: 'ref', 20 | SLOT: 'slot', 21 | EXPRESSION: 'expression', 22 | }); 23 | 24 | const ScriptEvent = Object.freeze({ 25 | ...CommonEvent, 26 | METHOD: 'method', 27 | COMPUTED: 'computed', 28 | IMPORTED_COMPONENT: 'imported-component', 29 | }); 30 | 31 | const ParserEvent = Object.freeze({ 32 | NAME: 'name', 33 | DESCRIPTION: 'description', 34 | KEYWORDS: 'keywords', 35 | 36 | DATA: 'data', 37 | EVENT: 'event', 38 | REF: 'ref', 39 | SLOT: 'slot', 40 | METHOD: 'method', 41 | COMPUTED: 'computed', 42 | IMPORTED_COMPONENT: 'component', 43 | 44 | FAILURE: 'failure', 45 | END: 'end', 46 | }); 47 | 48 | module.exports = { 49 | CommonEvent, 50 | TemplateEvent, 51 | ScriptEvent, 52 | ParserEvent 53 | }; 54 | -------------------------------------------------------------------------------- /lib/v3/parser.js: -------------------------------------------------------------------------------- 1 | const EventEmitter = require('events'); 2 | const path = require('path'); 3 | 4 | const utils = require('./../utils'); 5 | const { 6 | normalize: normalizeOptions, 7 | validateFeatures, 8 | } = require('../options'); 9 | 10 | const TemplateParser = require('./template'); 11 | const { TemplateEvent, ScriptEvent, ParserEvent } = require('./events'); 12 | 13 | const ScriptParser = require('./script'); 14 | 15 | /** 16 | * @typedef {import('../../typings').Svelte3Feature} Svelte3Feature 17 | * @type {Svelte3Feature[]} 18 | */ 19 | const SUPPORTED_FEATURES = [ 20 | 'name', 21 | 'data', 22 | 'computed', 23 | 'methods', 24 | 'components', 25 | 'description', 26 | 'keywords', 27 | 'events', 28 | 'slots', 29 | 'refs' 30 | ]; 31 | 32 | class Parser extends EventEmitter { 33 | /** 34 | * @param {import('../options').SvelteParserOptions} options 35 | */ 36 | constructor(options) { 37 | super(); 38 | 39 | Parser.validateOptions(options); 40 | 41 | // External options 42 | this.filename = options.filename; 43 | this.features = options.features; 44 | this.includeSourceLocations = options.includeSourceLocations; 45 | /** @type {import("../helpers").FileStructure} */ 46 | this.structure = options.structure; 47 | 48 | // Internal properties 49 | this.componentName = null; 50 | 51 | this.scriptParser = null; 52 | this.templateParser = null; 53 | } 54 | 55 | walk() { 56 | process.nextTick(() => { 57 | try { 58 | this.__walk(); 59 | } catch (error) { 60 | this.emit(ParserEvent.FAILURE, error); 61 | } 62 | }); 63 | 64 | return this; 65 | } 66 | 67 | __walk() { 68 | if (this.features.includes('name')) { 69 | this.parseComponentName(); 70 | } 71 | 72 | if (this.structure.scripts && this.structure.scripts.length) { 73 | this.parseScriptBlocks(this.structure.scripts); 74 | } 75 | 76 | if (this.structure.template) { 77 | this.parseTemplate(this.structure.template); 78 | } 79 | 80 | this.emit(ParserEvent.END); 81 | } 82 | 83 | static getDefaultOptions() { 84 | return { 85 | includeSourceLocations: true, 86 | features: [...SUPPORTED_FEATURES], 87 | }; 88 | } 89 | 90 | static validateOptions(options) { 91 | normalizeOptions(options, Parser.getDefaultOptions()); 92 | 93 | validateFeatures(options, SUPPORTED_FEATURES); 94 | } 95 | 96 | static getEventName(feature) { 97 | return feature.endsWith('s') 98 | ? feature.substring(0, feature.length - 1) 99 | : feature; 100 | } 101 | 102 | parseComponentName() { 103 | if (this.componentName === null) { 104 | if (this.filename) { 105 | this.componentName = path.parse(this.filename).name; 106 | } 107 | } 108 | 109 | if (this.componentName) { 110 | this.emit(ParserEvent.NAME, utils.buildCamelCase(this.componentName)); 111 | } 112 | } 113 | 114 | /** 115 | * @param {import('../../typings').IScopedCommentItem} comment 116 | */ 117 | emitGlobalComment(comment) { 118 | if (comment && utils.isTopLevelComment(comment)) { 119 | if (this.features.includes('description')) { 120 | this.emit(ParserEvent.DESCRIPTION, comment.description); 121 | } 122 | 123 | if (this.features.includes('keywords')) { 124 | this.emit(ParserEvent.KEYWORDS, comment.keywords); 125 | } 126 | } 127 | } 128 | 129 | parseScriptBlocks(scripts) { 130 | const scriptParser = this.buildScriptParser(); 131 | 132 | scriptParser.parse(scripts); 133 | 134 | return scriptParser; 135 | } 136 | 137 | parseTemplateJavascriptExpression(expression) { 138 | const scriptParser = this.buildScriptParser(); 139 | 140 | scriptParser.parseScriptExpression(expression); 141 | 142 | return scriptParser; 143 | } 144 | 145 | parseTemplate(template) { 146 | const templateParser = this.buildTemplateParser(); 147 | 148 | templateParser.parse(template); 149 | 150 | return templateParser; 151 | } 152 | 153 | buildScriptParser() { 154 | if (this.scriptParser) { 155 | return this.scriptParser; 156 | } 157 | 158 | this.scriptParser = new ScriptParser({ 159 | features: this.features, 160 | includeSourceLocations: this.includeSourceLocations 161 | }); 162 | 163 | this.subscribeToScriptParser(this.scriptParser); 164 | 165 | return this.scriptParser; 166 | } 167 | 168 | buildTemplateParser() { 169 | if (this.templateParser) { 170 | return this.templateParser; 171 | } 172 | 173 | this.templateParser = new TemplateParser({ 174 | features: this.features, 175 | includeSourceLocations: this.includeSourceLocations 176 | }); 177 | 178 | this.subscribeToTemplateParser(this.templateParser); 179 | 180 | return this.templateParser; 181 | } 182 | 183 | subscribeToScriptParser(scriptParser) { 184 | scriptParser.on(ScriptEvent.COMPUTED, item => { 185 | this.emit(ParserEvent.COMPUTED, item); 186 | }); 187 | scriptParser.on(ScriptEvent.DATA, item => { 188 | this.emit(ParserEvent.DATA, item); 189 | }); 190 | scriptParser.on(ScriptEvent.EVENT, item => { 191 | this.emit(ParserEvent.EVENT, item); 192 | }); 193 | scriptParser.on(ScriptEvent.IMPORTED_COMPONENT, item => { 194 | this.emit(ParserEvent.IMPORTED_COMPONENT, item); 195 | }); 196 | scriptParser.on(ScriptEvent.METHOD, item => { 197 | this.emit(ParserEvent.METHOD, item); 198 | }); 199 | 200 | // Special cases where more parsing is required 201 | scriptParser.on(ScriptEvent.GLOBAL_COMMENT, (comment) => { 202 | this.emitGlobalComment(comment); 203 | }); 204 | } 205 | 206 | subscribeToTemplateParser(templateParser) { 207 | // Forward emit of basic template events 208 | templateParser.on(TemplateEvent.DATA, dataItem => { 209 | this.emit(ParserEvent.DATA, dataItem); 210 | }); 211 | templateParser.on(TemplateEvent.EVENT, eventItem => { 212 | this.emit(ParserEvent.EVENT, eventItem); 213 | }); 214 | templateParser.on(TemplateEvent.NAME, name => { 215 | this.emit(ParserEvent.NAME, name); 216 | }); 217 | templateParser.on(TemplateEvent.REF, refItem => { 218 | this.emit(ParserEvent.REF, refItem); 219 | }); 220 | templateParser.on(TemplateEvent.SLOT, slotItem => { 221 | this.emit(ParserEvent.SLOT, slotItem); 222 | }); 223 | 224 | // Special cases where more parsing is required 225 | templateParser.on(TemplateEvent.EXPRESSION, (expression) => { 226 | this.parseTemplateJavascriptExpression(expression); 227 | }); 228 | templateParser.on(TemplateEvent.GLOBAL_COMMENT, (comment) => { 229 | this.emitGlobalComment(comment); 230 | }); 231 | } 232 | } 233 | 234 | module.exports = Parser; 235 | module.exports.SUPPORTED_FEATURES = SUPPORTED_FEATURES; 236 | -------------------------------------------------------------------------------- /lib/v3/template.js: -------------------------------------------------------------------------------- 1 | const EventEmitter = require('events'); 2 | const { Parser: HtmlParser } = require('htmlparser2-svelte'); 3 | 4 | const { parseAndMergeKeywords } = require('./v3-utils'); 5 | const { parseComment, hasOwnProperty } = require('../utils'); 6 | const { TemplateEvent } = require('./events'); 7 | const jsdoc = require('../jsdoc'); 8 | 9 | /** 10 | * @typedef {import('../../typings').Svelte3Feature} Svelte3Feature 11 | * 12 | * @typedef TemplateParserOptions 13 | * @property {Svelte3Feature[]} features 14 | * @property {boolean} includeSourceLocations 15 | */ 16 | class TemplateParser extends EventEmitter { 17 | /** 18 | * @param {TemplateParserOptions} options 19 | */ 20 | constructor(options) { 21 | super(); 22 | 23 | this.features = options.features; 24 | this.includeSourceLocations = options.includeSourceLocations; 25 | 26 | // Internal properties 27 | /** 28 | * Map of events already emitted. Check if an event exists in this map 29 | * before emitting it again. 30 | */ 31 | this.eventsEmitted = Object.create(null); // Empty Map 32 | } 33 | 34 | /** 35 | * Parse the template markup and produce events with parsed data. 36 | * @param {string} template The template markup to parse 37 | */ 38 | parse(template) { 39 | const options = { 40 | lowerCaseTags: false, 41 | lowerCaseAttributeNames: false, 42 | curlyBracesInAttributes: true 43 | }; 44 | 45 | const parser = new HtmlParser(this.getTemplateHandler(), options); 46 | 47 | parser.write(template); 48 | parser.end(); 49 | } 50 | 51 | getTemplateHandler() { 52 | let rootElementIndex = 0; 53 | let lastComment = null; 54 | let lastAttributeIndex = 0; 55 | let lastAttributeLocations = {}; 56 | let lastTagName = null; 57 | let parser = null; 58 | 59 | const parseAndConsumeLastComment = (defaultVisibility) => { 60 | const parsedComment = parseComment( 61 | jsdoc.convertToJsDocComment(lastComment), 62 | defaultVisibility 63 | ); 64 | 65 | lastComment = null; 66 | 67 | return parsedComment; 68 | }; 69 | 70 | return { 71 | onparserinit: (parserInstance) => { 72 | parser = parserInstance; 73 | }, 74 | oncomment: (data) => { 75 | lastComment = data.trim(); 76 | }, 77 | ontext: (text) => { 78 | if (text.trim()) { 79 | lastComment = null; 80 | } 81 | }, 82 | onattribute: (name, value) => { 83 | if (this.includeSourceLocations && parser.startIndex >= 0 && parser.endIndex >= parser.startIndex) { 84 | lastAttributeLocations[name] = { 85 | start: lastAttributeIndex, 86 | end: parser._tokenizer._index 87 | }; 88 | 89 | lastAttributeIndex = parser._tokenizer._index; 90 | } 91 | 92 | if (this.features.includes('events')) { 93 | if (lastTagName !== 'slot') { 94 | // Expose events that propagated from child events 95 | // Handle event syntax like `````` 96 | if (name.length > 3 && name.indexOf('on:') === 0 && !value) { 97 | const nameWithModificators = name.substr(3).split('|'); 98 | 99 | const parsedComment = parseAndConsumeLastComment(); 100 | 101 | const baseEvent = { 102 | ...parsedComment, 103 | name: nameWithModificators[0], 104 | parent: lastTagName, 105 | modificators: nameWithModificators.slice(1), 106 | locations: this.includeSourceLocations && hasOwnProperty(lastAttributeLocations, name) 107 | ? [lastAttributeLocations[name]] 108 | : null 109 | }; 110 | 111 | if (!hasOwnProperty(this.eventsEmitted, baseEvent.name)) { 112 | this.eventsEmitted[baseEvent.name] = baseEvent; 113 | 114 | parseAndMergeKeywords(parsedComment.keywords, baseEvent); 115 | 116 | this.emit(TemplateEvent.EVENT, baseEvent); 117 | } 118 | 119 | lastComment = null; 120 | } 121 | 122 | // Parse event handlers 123 | if (name.length > 3 && name.indexOf('on:') === 0 && value) { 124 | this.emit(TemplateEvent.EXPRESSION, value); 125 | } 126 | } 127 | } 128 | }, 129 | onopentagname: (tagName) => { 130 | lastTagName = tagName; 131 | lastAttributeIndex = parser._tokenizer._index; 132 | lastAttributeLocations = {}; 133 | }, 134 | onopentag: (tagName, attrs) => { 135 | const isNotStyleOrScript = !['style', 'script'].includes(tagName); 136 | const isTopLevelElement = parser._stack.length === 1; 137 | 138 | if (isTopLevelElement && isNotStyleOrScript) { 139 | if (lastComment && rootElementIndex === 0) { 140 | const parsedComment = parseAndConsumeLastComment(); 141 | 142 | this.emit(TemplateEvent.GLOBAL_COMMENT, parsedComment); 143 | } 144 | 145 | rootElementIndex += 1; 146 | } 147 | 148 | if (tagName === 'slot') { 149 | if (this.features.includes('slots')) { 150 | const exposedParameters = Object.keys(attrs) 151 | .filter(name => name.length > 0 && name !== 'name') 152 | .map(name => ({ 153 | name: name, 154 | type: jsdoc.getDefaultJSDocType() 155 | })); 156 | 157 | const parsedComment = parseAndConsumeLastComment('public'); 158 | 159 | /** @type {import('../../typings').SvelteSlotItem} */ 160 | const slot = { 161 | ...parsedComment, 162 | name: attrs.name || 'default', 163 | visibility: 'public', 164 | params: exposedParameters 165 | }; 166 | 167 | parseAndMergeKeywords(parsedComment.keywords, slot, false); 168 | 169 | // TODO 5.* | Backward compatilibity 170 | slot.parameters = slot.params; 171 | 172 | if (this.includeSourceLocations && parser.startIndex >= 0 && parser.endIndex >= parser.startIndex) { 173 | slot.loc = { 174 | start: parser.startIndex, 175 | end: parser.endIndex 176 | }; 177 | } 178 | 179 | this.emit(TemplateEvent.SLOT, slot); 180 | } 181 | } else { 182 | if (tagName === 'svelte:options' && attrs.tag) { 183 | if (this.features.includes('name')) { 184 | this.emit(TemplateEvent.NAME, attrs.tag); 185 | } 186 | } 187 | 188 | if (this.features.includes('data')) { 189 | const bindProperties = Object.keys(attrs) 190 | .filter(name => name.length > 5 && name.indexOf('bind:') === 0) 191 | .filter(name => name !== 'bind:this') 192 | .map(name => { 193 | const sourcePropertyName = name.substr(5); 194 | 195 | let targetPropertyName = sourcePropertyName; 196 | 197 | const attributeValue = attrs[name]; 198 | 199 | if (attributeValue && attributeValue.length > 0) { 200 | targetPropertyName = attributeValue; 201 | } 202 | 203 | return { 204 | sourcePropertyName: sourcePropertyName, 205 | targetPropertyName: targetPropertyName, 206 | parent: tagName, 207 | locations: this.includeSourceLocations && hasOwnProperty(lastAttributeLocations, name) 208 | ? [lastAttributeLocations[name]] 209 | : null 210 | }; 211 | }); 212 | 213 | bindProperties.forEach(bindProperty => { 214 | const dataItem = { 215 | name: bindProperty.targetPropertyName, 216 | kind: undefined, 217 | bind: [{ 218 | source: bindProperty.parent, 219 | property: bindProperty.sourcePropertyName 220 | }], 221 | locations: bindProperty.locations, 222 | visibility: 'private', 223 | static: false, 224 | readonly: false 225 | }; 226 | 227 | this.emit(TemplateEvent.DATA, dataItem); 228 | }); 229 | } 230 | 231 | if (this.features.includes('refs')) { 232 | if (hasOwnProperty(attrs, 'bind:this') && attrs['bind:this']) { 233 | const bindedVariableName = attrs['bind:this']; 234 | 235 | const refItem = { 236 | visibility: 'private', 237 | name: bindedVariableName, 238 | parent: tagName, 239 | locations: this.includeSourceLocations && hasOwnProperty(lastAttributeLocations, 'bind:this') 240 | ? [lastAttributeLocations['bind:this']] 241 | : null 242 | }; 243 | 244 | this.emit(TemplateEvent.REF, refItem); 245 | } 246 | } 247 | } 248 | } 249 | }; 250 | } 251 | } 252 | 253 | module.exports = TemplateParser; 254 | module.exports.TemplateEvent = TemplateEvent; 255 | -------------------------------------------------------------------------------- /lib/v3/v3-utils.js: -------------------------------------------------------------------------------- 1 | const { 2 | parseParamKeyword, 3 | parseReturnKeyword, 4 | parseTypeKeyword 5 | } = require('../jsdoc'); 6 | 7 | const { inferTypeFromVariableDeclaration } = require('../utils'); 8 | 9 | class InvalidNodeTypeError extends TypeError { 10 | constructor(expected, actual = '', ...args) { 11 | super(expected, actual, ...args); 12 | this.message = `Expected node type to be '${expected}', but it was '${actual}'.`; 13 | } 14 | } 15 | 16 | /** 17 | * @typedef {{ type: string }} AstNode 18 | */ 19 | 20 | /** 21 | * @throws InvalidNodeTypeError if the node is not of the correct type 22 | * @param {AstNode} node 23 | * @param {string} type 24 | */ 25 | function assertNodeType(node, type) { 26 | if (node.type !== type) { 27 | throw new InvalidNodeTypeError(type, node.type); 28 | } 29 | } 30 | 31 | /** All `@param` JSDoc aliases. */ 32 | const PARAM_ALIASES = { 33 | arg: true, 34 | argument: true, 35 | param: true 36 | }; 37 | 38 | /** All `@returns` JSDoc aliases. */ 39 | const RETURN_ALIASES = { 40 | return: true, 41 | returns: true, 42 | }; 43 | 44 | /** 45 | * Returns the innermost body prop from node. 46 | * returns the same node if it does not have a body. 47 | * @param {AstNode & { body?: AstNode | AstNode[] }} node an AST node 48 | */ 49 | function getInnermostBody(node) { 50 | while (node && node.body) { 51 | node = node.body; 52 | } 53 | 54 | return node; 55 | } 56 | 57 | /** 58 | * 59 | * @param {AstNode} nodeParams Array of node parameters 60 | * @return {import('../../typings').SvelteMethodParamItem[]} 61 | */ 62 | function parseParams(nodeParams) { 63 | /** @type {import('../../typings').SvelteMethodParamItem[]} */ 64 | const params = []; 65 | 66 | if (nodeParams && nodeParams.length) { 67 | nodeParams.forEach((param) => { 68 | if (param.type === 'Identifier') { 69 | params.push({ 70 | name: param.name 71 | }); 72 | } 73 | 74 | if (param.type === 'AssignmentPattern') { 75 | const inferredType = inferTypeFromVariableDeclaration({ 76 | // fake variable declarator block 77 | declarator: { 78 | init: param.right 79 | } 80 | }); 81 | 82 | params.push({ 83 | name: param.left.name, 84 | type: inferredType, 85 | description: null, 86 | optional: true, 87 | defaultValue: param.right.raw, 88 | }); 89 | } 90 | }); 91 | } 92 | 93 | return params; 94 | } 95 | 96 | /** 97 | * @param {AstNode} node a 'FunctionDeclaration' AST node 98 | */ 99 | function parseFunctionDeclaration(node) { 100 | assertNodeType(node, 'FunctionDeclaration'); 101 | 102 | const params = parseParams(node.params); 103 | 104 | const output = { 105 | node: node, 106 | name: node.id.name, 107 | location: { 108 | start: node.id.start, 109 | end: node.id.end 110 | }, 111 | params: params, 112 | }; 113 | 114 | return output; 115 | } 116 | 117 | /** 118 | * @param {AstNode} node a 'VariableDeclaration' AST node 119 | */ 120 | function parseVariableDeclaration(node) { 121 | assertNodeType(node, 'VariableDeclaration'); 122 | 123 | const result = []; 124 | 125 | node.declarations.forEach(declarator => { 126 | const idNode = declarator.id; 127 | const init = declarator.init; 128 | 129 | if (idNode.type === 'Identifier') { 130 | const data = { 131 | name: idNode.name, 132 | kind: node.kind, 133 | node: node, 134 | declarator: declarator, 135 | location: { 136 | start: idNode.start, 137 | end: idNode.end 138 | } 139 | }; 140 | 141 | if ( 142 | init && 143 | init.params && 144 | ( 145 | init.type === 'FunctionExpression' || 146 | init.type === 'ArrowFunctionExpression' 147 | ) 148 | ) { 149 | data.params = parseParams(init.params); 150 | } 151 | 152 | result.push(data); 153 | } else if (idNode.type === 'ObjectPattern') { 154 | idNode.properties.forEach(propertyNode => { 155 | const propertyIdNode = propertyNode.key; 156 | 157 | if (propertyIdNode.type === 'Identifier') { 158 | result.push({ 159 | name: propertyIdNode.name, 160 | kind: node.kind, 161 | node: node, 162 | declarator: declarator, 163 | locations: { 164 | start: propertyIdNode.start, 165 | end: propertyIdNode.end 166 | } 167 | }); 168 | } 169 | }); 170 | } 171 | }); 172 | 173 | return result; 174 | } 175 | 176 | /** 177 | * @sideEffect Mutates `item.type`. 178 | * 179 | * Updates `item.type` from `@type` in `item.keywords`, if any. 180 | * 181 | * @param {{ name: string; description: string}[]} item 182 | */ 183 | function updateType(item) { 184 | const typeKeyword = item.keywords.find(kw => kw.name === 'type'); 185 | 186 | if (typeKeyword) { 187 | const parsedType = parseTypeKeyword(typeKeyword.description); 188 | 189 | if (parsedType) { 190 | item.type = parsedType; 191 | } 192 | } 193 | } 194 | 195 | /** 196 | * @sideEffect Mutates `event.params` and `event.return`. 197 | * 198 | * Parses the `keywords` argument and merges the result in `event`. 199 | * 200 | * If a param exists as a JSDoc `@param`, it will overwrite the param of the 201 | * same name already in event, if any. If a `@param` in JSDoc does 202 | * not match the name of an actual param of the function, it is appended at 203 | * end of the `event` params. 204 | * 205 | * @param {{ name: string; description: string}[]} keywords 206 | * @param {{ params?: any[]; return?: any }} event 207 | * @param {string} [allowToParseReturn=true] 208 | */ 209 | function parseAndMergeKeywords(keywords = [], event, allowToParseReturn = true) { 210 | if (!event.params) { 211 | event.params = []; 212 | } 213 | 214 | keywords.forEach(({ name, description }) => { 215 | if (name in PARAM_ALIASES) { 216 | const parsedParam = parseParamKeyword(description); 217 | const pIndex = event.params.findIndex( 218 | p => p.name === parsedParam.name 219 | ); 220 | 221 | /* 222 | * Replace the param if there is already one present with 223 | * the same name. This will happen with parsed 224 | * FunctionDeclaration because params will already be 225 | * present from parsing the AST node. 226 | */ 227 | if (pIndex >= 0) { 228 | if ( 229 | parsedParam.type && 230 | event.params[pIndex].type && 231 | parsedParam.type.text === '*' 232 | ) { 233 | /** 234 | * Only `getDefaultJSDocType()` has type.text = "*" 235 | * so, if parsedParams contain that, we can be sure that 236 | * type information was not found in the keyword description 237 | * 238 | * When that happens, we check if type data is already present 239 | * from parsing the assignment pattern in the FunctionDeclaration 240 | */ 241 | parsedParam.type = event.params[pIndex].type; 242 | parsedParam.optional = event.params[pIndex].optional; 243 | } 244 | 245 | event.params[pIndex] = Object.assign(event.params[pIndex], parsedParam); 246 | } else { 247 | /* 248 | * This means @param does not match an actual param 249 | * in the FunctionDeclaration. For now, we keep it. 250 | * TODO: Implement option to choose behaviour (keep, ignore, warn, throw) 251 | */ 252 | event.params.push(parsedParam); 253 | } 254 | } else if (name in RETURN_ALIASES) { 255 | if (allowToParseReturn) { 256 | event.return = parseReturnKeyword(description); 257 | } 258 | } 259 | }); 260 | 261 | if (event.params.length === 0) { 262 | delete event.params; 263 | } 264 | } 265 | 266 | module.exports = { 267 | assertNodeType, 268 | getInnermostBody, 269 | parseFunctionDeclaration, 270 | parseVariableDeclaration, 271 | parseAndMergeKeywords, 272 | updateType, 273 | }; 274 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sveltedoc-parser", 3 | "version": "4.4.0", 4 | "description": "Generate a JSON documentation for a Svelte file", 5 | "main": "index.js", 6 | "types": "./typings.d.ts", 7 | "scripts": { 8 | "lint": "eslint ./**/*.js", 9 | "test": "mocha ./test/**/*.spec.js", 10 | "prepublishOnly": "npm run test && npm run lint", 11 | "test:unit": "mocha ./test/unit/**/*.spec.js", 12 | "test:integration": "mocha ./test/**/integration/**/*.spec.js", 13 | "test:integration-overall": "mocha ./test/**/integration/overall/overall.main.spec.js", 14 | "test:svelte2": "mocha ./test/svelte2/**/*.spec.js", 15 | "test:svelte2:unit": "mocha ./test/svelte2/unit/**/*.spec.js", 16 | "test:svelte2:integration": "mocha ./test/svelte2/integration/**/*.spec.js", 17 | "test:svelte3": "mocha ./test/svelte3/**/*.spec.js", 18 | "test:svelte3:unit": "mocha ./test/svelte3/unit/**/*.spec.js", 19 | "test:svelte3:integration": "mocha ./test/svelte3/integration/**/*.spec.js", 20 | "generate-examples": "node ./scripts/generate-examples.js" 21 | }, 22 | "repository": { 23 | "type": "git", 24 | "url": "git+https://github.com/alexprey/sveltedoc-parser.git" 25 | }, 26 | "keywords": [ 27 | "sveltedoc", 28 | "sveltefile", 29 | "svelte", 30 | "doc", 31 | "parser", 32 | "jsdoc" 33 | ], 34 | "author": "Alexey Mulyukin", 35 | "license": "MIT", 36 | "bugs": { 37 | "url": "https://github.com/alexprey/sveltedoc-parser/issues" 38 | }, 39 | "homepage": "https://github.com/alexprey/sveltedoc-parser", 40 | "dependencies": { 41 | "espree": "9.2.0", 42 | "eslint": "8.4.1", 43 | "htmlparser2-svelte": "4.1.0" 44 | }, 45 | "devDependencies": { 46 | "chai": "4.1.2", 47 | "dirty-chai": "2.0.1", 48 | "eslint": "8.4.1", 49 | "eslint-plugin-chai-expect": "3.0.0", 50 | "eslint-plugin-import": "2.25.3", 51 | "eslint-plugin-node": "11.1.0", 52 | "eslint-plugin-promise": "5.2.0", 53 | "mocha": "9.1.3", 54 | "glob": "7.2.0" 55 | }, 56 | "engines": { 57 | "node": ">=10.0.0" 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /scripts/generate-examples.js: -------------------------------------------------------------------------------- 1 | const glob = require('glob'); 2 | const path = require('path'); 3 | const fs = require('fs'); 4 | 5 | const parser = require('../index'); 6 | 7 | function getFileName(input) { 8 | const basename = path.basename(input); 9 | const dotIndex = basename.lastIndexOf('.'); 10 | 11 | if (dotIndex > 0) { 12 | return basename.substring(0, dotIndex); 13 | } 14 | 15 | return basename; 16 | } 17 | 18 | function processSvelteFile(svelteFilePath) { 19 | return new Promise(resolve => { 20 | parser.parse({ 21 | version: 3, 22 | filename: svelteFilePath, 23 | ignoredVisibilities: [] 24 | }).then(doc => { 25 | const targetFilePath = path.join(path.dirname(svelteFilePath), `${getFileName(svelteFilePath)}.json`); 26 | 27 | fs.writeFile( 28 | targetFilePath, 29 | JSON.stringify(doc, undefined, 4), 30 | (err) => { 31 | if (err) { 32 | console.log(`[Error] ${svelteFilePath} (${err.message})`); 33 | resolve({ err, svelteFilePath }); 34 | 35 | return; 36 | } 37 | 38 | console.log('[Done]', svelteFilePath); 39 | resolve({ svelteFilePath }); 40 | }); 41 | }).catch(err => { 42 | console.log(`[Error] ${svelteFilePath} (${err.message})`); 43 | resolve({ err, svelteFilePath }); 44 | }); 45 | }); 46 | } 47 | 48 | glob('./examples/**/*.svelte', (err, files) => { 49 | if (err) { 50 | throw err; 51 | } 52 | 53 | const promises = files.map(svelteFilePath => { 54 | return processSvelteFile(svelteFilePath); 55 | }); 56 | 57 | Promise.all(promises) 58 | .then(errors => { 59 | if (errors) { 60 | errors.forEach(({ err, svelteFilePath }) => { 61 | if (err) { 62 | console.error(`\nError at file processing "${svelteFilePath}":`, err); 63 | } 64 | }); 65 | } 66 | 67 | process.exit(errors.filter(({ err }) => err).length); 68 | }); 69 | }); 70 | -------------------------------------------------------------------------------- /test/integration/detector/detector.spec.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const fs = require('fs'); 3 | const chai = require('chai'); 4 | const expect = chai.expect; 5 | 6 | const detector = require('./../../../lib/detector'); 7 | 8 | describe('SvelteDoc file version detector', () => { 9 | it('Empty file detected with undefined version', () => { 10 | const version = detector.detectVersionFromOptions({ 11 | filename: path.join(__dirname, 'empty.svelte') 12 | }); 13 | 14 | expect(version).is.undefined; 15 | }); 16 | 17 | describe('Svelte V2 files check', () => { 18 | const folderPath = path.resolve(__dirname, 'v2'); 19 | const files = fs.readdirSync(folderPath); 20 | 21 | files 22 | .filter(file => file.endsWith('.svelte')) 23 | .forEach(file => { 24 | it('Svelte V2 file: ' + file, () => { 25 | const version = detector.detectVersionFromOptions({ 26 | filename: path.join(folderPath, file) 27 | }); 28 | 29 | expect(version).is.equal(detector.SVELTE_VERSION_2); 30 | }); 31 | }); 32 | }); 33 | 34 | describe('Svelte V3 files check', () => { 35 | const folderPath = path.resolve(__dirname, 'v3'); 36 | const files = fs.readdirSync(folderPath); 37 | 38 | files 39 | .filter(file => file.endsWith('.svelte')) 40 | .forEach(file => { 41 | it('Svelte V3 file: ' + file, () => { 42 | const version = detector.detectVersionFromOptions({ 43 | filename: path.join(folderPath, file) 44 | }); 45 | 46 | expect(version).is.equal(detector.SVELTE_VERSION_3); 47 | }); 48 | }); 49 | }); 50 | }); 51 | -------------------------------------------------------------------------------- /test/integration/detector/empty.svelte: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KatChaotic/sveltedoc-parser/7b7d0ff777731ce8ed35ddead97eaa85afe9e82f/test/integration/detector/empty.svelte -------------------------------------------------------------------------------- /test/integration/detector/v2/elseif.svelte: -------------------------------------------------------------------------------- 1 |
2 | {#if rnd() < 0.3} 3 | Completed with first try 4 | {:elseif rnd() < 0.7} 5 | Completed with second try 6 | {/if} 7 |
8 | -------------------------------------------------------------------------------- /test/integration/detector/v2/exportDefault.svelte: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/integration/detector/v2/ref.svelte: -------------------------------------------------------------------------------- 1 |
2 | Main content that can be referenced from another code 3 |
-------------------------------------------------------------------------------- /test/integration/detector/v3/context.svelte: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/integration/detector/v3/contextAndScript.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 | -------------------------------------------------------------------------------- /test/integration/detector/v3/elseif.svelte: -------------------------------------------------------------------------------- 1 |
2 | {#if rnd() < 0.3} 3 | Completed with first try 4 | {:else if rnd() < 0.7} 5 | Completed with second try 6 | {/if} 7 |
8 | -------------------------------------------------------------------------------- /test/integration/detector/v3/letInSlot.svelte: -------------------------------------------------------------------------------- 1 | 2 |
{item.text}
3 |
4 | 5 | -------------------------------------------------------------------------------- /test/integration/parse/basicV2.svelte: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /test/integration/parse/basicV3.svelte: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/integration/parse/parse.spec.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const chai = require('chai'); 3 | const expect = chai.expect; 4 | 5 | const parser = require('./../../../index'); 6 | const { ParserError } = require('../../../lib/options'); 7 | const { AssertionError } = require('chai'); 8 | const { 9 | SUPPORTED_FEATURES: V3_SUPPORTED_FEATURES 10 | } = require('../../../lib/v3/parser'); 11 | const { 12 | SUPPORTED_FEATURES: V2_SUPPORTED_FEATURES 13 | } = require('../../../lib/parser'); 14 | 15 | describe('parse - Integration', () => { 16 | it('should correctly auto-detect svelte V2 component', (done) => { 17 | parser.parse({ 18 | filename: path.resolve(__dirname, 'basicV2.svelte'), 19 | }).then((doc) => { 20 | expect(doc, 'Document should exist').to.exist; 21 | // v3-parser converts component name to kebab-case 22 | expect(doc.name).to.equal('basic-v2'); 23 | 24 | done(); 25 | }).catch(e => { 26 | done(e); 27 | }); 28 | }); 29 | 30 | it('should correctly auto-detect svelte V3 component', (done) => { 31 | parser.parse({ 32 | filename: path.resolve(__dirname, 'basicV3.svelte'), 33 | }).then((doc) => { 34 | expect(doc, 'Document should exist').to.exist; 35 | // v3-parser converts component name to PascalCase 36 | expect(doc.name).to.equal('BasicV3'); 37 | 38 | done(); 39 | }).catch(e => { 40 | done(e); 41 | }); 42 | }); 43 | 44 | it('should throw when svelte V2 parser receives unsupported features', (done) => { 45 | parser.parse({ 46 | version: 2, 47 | filename: path.resolve(__dirname, 'basicV2.svelte'), 48 | features: ['data', 'unsupported'], 49 | }).then(() => { 50 | done(new AssertionError( 51 | 'parser.parse should throw ParserError.FeaturesNotSupported' 52 | )); 53 | }).catch(e => { 54 | expect(e.message).is.equal(ParserError.FeaturesNotSupported( 55 | ['unsupported'], V2_SUPPORTED_FEATURES 56 | )); 57 | done(); 58 | }).catch(e => { 59 | done(e); 60 | }); 61 | }); 62 | 63 | it('should throw when svelte V3 parser receives unsupported features', (done) => { 64 | parser.parse({ 65 | version: 3, 66 | filename: path.resolve(__dirname, 'basicV3.svelte'), 67 | features: ['data', 'unsupported'], 68 | }).then(() => { 69 | done(new AssertionError( 70 | 'parser.parse should throw ParserError.FeaturesNotSupported' 71 | )); 72 | }).catch(e => { 73 | expect(e.message).is.equal(ParserError.FeaturesNotSupported( 74 | ['unsupported'], V3_SUPPORTED_FEATURES 75 | )); 76 | done(); 77 | }).catch(e => { 78 | done(e); 79 | }); 80 | }); 81 | }); 82 | -------------------------------------------------------------------------------- /test/svelte2/integration/basics/basic.description.svelte: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | -------------------------------------------------------------------------------- /test/svelte2/integration/basics/basic.spec.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const chai = require('chai'); 3 | const expect = chai.expect; 4 | 5 | const parser = require('../../../../index'); 6 | 7 | describe('SvelteDoc - Basics', () => { 8 | it('Component name should be extracted', (done) => { 9 | parser.parse({ 10 | version: 2, 11 | filename: path.resolve(__dirname, 'basic.description.svelte'), 12 | features: ['name'], 13 | ignoredVisibilities: [] 14 | }).then((doc) => { 15 | expect(doc, 'Document should be provided').to.exist; 16 | expect(doc.name).to.be.equal('basic.description'); 17 | 18 | done(); 19 | }).catch(e => { 20 | done(e); 21 | }); 22 | }); 23 | 24 | it('Component description should be parsed', (done) => { 25 | parser.parse({ 26 | version: 2, 27 | filename: path.resolve(__dirname, 'basic.description.svelte'), 28 | features: ['description'], 29 | ignoredVisibilities: [] 30 | }).then((doc) => { 31 | expect(doc, 'Document should be provided').to.exist; 32 | expect(doc.description).to.be.equal('The awesome svelte component description.'); 33 | 34 | done(); 35 | }).catch(e => { 36 | done(e); 37 | }); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /test/svelte2/integration/data/data.description.svelte: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | -------------------------------------------------------------------------------- /test/svelte2/integration/data/data.plain.svelte: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | -------------------------------------------------------------------------------- /test/svelte2/integration/data/data.spec.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const chai = require('chai'); 3 | const expect = chai.expect; 4 | 5 | const parser = require('../../../../index'); 6 | 7 | describe('SvelteDoc - Component data model', () => { 8 | it('Data model should be parsed', (done) => { 9 | parser.parse({ 10 | version: 2, 11 | filename: path.resolve(__dirname, 'data.plain.svelte'), 12 | features: ['data'], 13 | ignoredVisibilities: [] 14 | }).then((doc) => { 15 | expect(doc, 'Document should be provided').to.exist; 16 | expect(doc.data).to.be.exist; 17 | 18 | const properties = doc.data; 19 | 20 | expect(properties.length).to.be.equal(1); 21 | 22 | const property = properties[0]; 23 | 24 | expect(property.name).to.be.equal('inlineTest'); 25 | expect(property.visibility).to.be.equal('public'); 26 | 27 | done(); 28 | }).catch(e => { 29 | done(e); 30 | }); 31 | }); 32 | 33 | it('Description for data properties should be parsed', (done) => { 34 | parser.parse({ 35 | version: 2, 36 | filename: path.resolve(__dirname, 'data.description.svelte'), 37 | features: ['data'], 38 | ignoredVisibilities: [] 39 | }).then((doc) => { 40 | expect(doc, 'Document should be provided').to.exist; 41 | expect(doc.data).to.be.exist; 42 | 43 | const properties = doc.data; 44 | 45 | expect(properties.length).to.be.equal(1); 46 | 47 | const property = properties[0]; 48 | 49 | expect(property.name).to.be.equal('inlineTest'); 50 | expect(property.description).to.be.equal('Description for inline test property.'); 51 | expect(property.visibility).to.be.equal('public'); 52 | 53 | done(); 54 | }).catch(e => { 55 | done(e); 56 | }); 57 | }); 58 | 59 | it('Type of data property should be parsed from JSDoc', (done) => { 60 | parser.parse({ 61 | version: 2, 62 | filename: path.resolve(__dirname, 'data.typeFromDescription.svelte'), 63 | features: ['data'], 64 | ignoredVisibilities: [] 65 | }).then((doc) => { 66 | expect(doc, 'Document should be provided').to.exist; 67 | expect(doc.data).to.be.exist; 68 | 69 | const properties = doc.data; 70 | 71 | expect(properties.length).to.be.equal(2); 72 | 73 | const firstProperty = properties.find(p => p.name === 'inlineTest'); 74 | 75 | expect(firstProperty.name).to.be.equal('inlineTest'); 76 | expect(firstProperty.type).to.be.exist; 77 | expect(firstProperty.type).to.be.deep.equal({ 78 | kind: 'type', 79 | text: 'string', 80 | type: 'string' 81 | }); 82 | expect(firstProperty.visibility).to.be.equal('public'); 83 | 84 | const secondProperty = properties.find(p => p.name === 'kind'); 85 | 86 | expect(secondProperty.name).to.be.equal('kind'); 87 | expect(secondProperty.type).to.be.exist; 88 | expect(secondProperty.type).to.be.deep.equal({ 89 | kind: 'union', 90 | text: '\'white\'|\'black\'', 91 | type: [ 92 | { 93 | kind: 'const', 94 | text: '\'white\'', 95 | type: 'string', 96 | value: 'white' 97 | }, 98 | { 99 | kind: 'const', 100 | text: '\'black\'', 101 | type: 'string', 102 | value: 'black' 103 | }, 104 | ] 105 | }); 106 | expect(secondProperty.visibility).to.be.equal('public'); 107 | 108 | done(); 109 | }).catch(e => { 110 | done(e); 111 | }); 112 | }); 113 | 114 | it('Type of data property should be parsed from JSDoc', (done) => { 115 | parser.parse({ 116 | version: 2, 117 | filename: path.resolve(__dirname, 'data.typeFromValue.svelte'), 118 | features: ['data'], 119 | ignoredVisibilities: [] 120 | }).then((doc) => { 121 | expect(doc, 'Document should be provided').to.exist; 122 | expect(doc.data).to.be.exist; 123 | 124 | const properties = doc.data; 125 | 126 | expect(properties.length).to.be.equal(4); 127 | 128 | const validatePropertyType = (propertyName, expectedType) => { 129 | const property = properties.find(p => p.name === propertyName); 130 | 131 | expect(property, `Data property "${propertyName}" should be exist`).to.be.exist; 132 | expect(property.type, `Type property for "${propertyName}" should be exist`).to.be.exist; 133 | expect(property.type, `Type property for "${propertyName}" should be parsed`).to.be.deep.equal({ 134 | kind: 'type', 135 | text: expectedType, 136 | type: expectedType 137 | }); 138 | }; 139 | 140 | validatePropertyType('string', 'string'); 141 | validatePropertyType('number', 'number'); 142 | validatePropertyType('emptyArray', 'Array'); 143 | validatePropertyType('emptyObject', 'any'); 144 | 145 | done(); 146 | }).catch(e => { 147 | done(e); 148 | }); 149 | }); 150 | }); 151 | -------------------------------------------------------------------------------- /test/svelte2/integration/data/data.typeFromDescription.svelte: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | -------------------------------------------------------------------------------- /test/svelte2/integration/data/data.typeFromValue.svelte: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | -------------------------------------------------------------------------------- /test/svelte2/integration/events/event.markup.fire.svelte: -------------------------------------------------------------------------------- 1 |
2 | 3 | 6 |
7 | 8 | -------------------------------------------------------------------------------- /test/svelte2/integration/events/event.markup.modificators.svelte: -------------------------------------------------------------------------------- 1 |
2 | 3 | 6 |
-------------------------------------------------------------------------------- /test/svelte2/integration/events/event.markup.propogate.svelte: -------------------------------------------------------------------------------- 1 |
2 | 3 | 6 |
7 | 8 | -------------------------------------------------------------------------------- /test/svelte2/integration/events/event.method.fire.identifier.svelte: -------------------------------------------------------------------------------- 1 |
2 | 5 |
6 | 7 | -------------------------------------------------------------------------------- /test/svelte2/integration/events/event.method.fire.svelte: -------------------------------------------------------------------------------- 1 |
2 | 5 |
6 | 7 | -------------------------------------------------------------------------------- /test/svelte2/integration/events/events.spec.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const chai = require('chai'); 3 | const expect = chai.expect; 4 | 5 | const parser = require('../../../../index'); 6 | 7 | describe('SvelteDoc - Events', () => { 8 | it('Fired events in markup should be parsed', (done) => { 9 | parser.parse({ 10 | version: 2, 11 | filename: path.resolve(__dirname, 'event.markup.fire.svelte'), 12 | features: ['events'], 13 | ignoredVisibilities: [] 14 | }).then((doc) => { 15 | expect(doc, 'Document should be provided').to.exist; 16 | expect(doc.events, 'Document events should be parsed').to.exist; 17 | 18 | expect(doc.events.length).to.equal(1); 19 | const event = doc.events[0]; 20 | 21 | expect(event, 'Event should be a valid entity').to.exist; 22 | expect(event.name).to.equal('click'); 23 | expect(event.visibility).to.equal('public'); 24 | expect(event.parent).to.be.null; 25 | expect(event.description).to.equal('Event fired when user clicked on button.'); 26 | 27 | done(); 28 | }).catch(e => { 29 | done(e); 30 | }); 31 | }); 32 | 33 | it('Fired events in component methods should be parsed', (done) => { 34 | parser.parse({ 35 | version: 2, 36 | filename: path.resolve(__dirname, 'event.method.fire.svelte'), 37 | features: ['events'], 38 | ignoredVisibilities: [] 39 | }).then((doc) => { 40 | expect(doc, 'Document should be provided').to.exist; 41 | expect(doc.events, 'Document events should be parsed').to.exist; 42 | 43 | expect(doc.events.length).to.equal(1); 44 | const event = doc.events[0]; 45 | 46 | expect(event, 'Event should be a valid entity').to.exist; 47 | expect(event.name).to.equal('click'); 48 | expect(event.visibility).to.equal('public'); 49 | expect(event.parent).to.be.null; 50 | expect(event.description).to.equal('Event fired when user clicked on button.'); 51 | 52 | done(); 53 | }).catch(e => { 54 | done(e); 55 | }); 56 | }); 57 | 58 | it('Fired events with identifier event name in component methods should be parsed', (done) => { 59 | parser.parse({ 60 | version: 2, 61 | filename: path.resolve(__dirname, 'event.method.fire.identifier.svelte'), 62 | features: ['events'], 63 | ignoredVisibilities: [] 64 | }).then((doc) => { 65 | expect(doc, 'Document should be provided').to.exist; 66 | expect(doc.events, 'Document events should be parsed').to.exist; 67 | 68 | expect(doc.events.length).to.equal(2); 69 | const event = doc.events[0]; 70 | 71 | expect(event, 'Event should be a valid entity').to.exist; 72 | expect(event.name).to.equal('click'); 73 | expect(event.visibility).to.equal('public'); 74 | expect(event.parent).to.be.null; 75 | expect(event.description).to.equal('Event fired when user clicked on button.'); 76 | 77 | const event2 = doc.events[1]; 78 | 79 | expect(event2, 'Event should be a valid entity').to.exist; 80 | expect(event2.name).to.equal('press'); 81 | expect(event2.visibility).to.equal('public'); 82 | expect(event2.parent).to.be.null; 83 | expect(event2.description).to.equal('Event fired when user pressed on button.'); 84 | 85 | done(); 86 | }).catch(e => { 87 | done(e); 88 | }); 89 | }); 90 | 91 | it('Propogated events in markup should be parsed', (done) => { 92 | parser.parse({ 93 | version: 2, 94 | filename: path.resolve(__dirname, 'event.markup.propogate.svelte'), 95 | features: ['events'], 96 | ignoredVisibilities: [] 97 | }).then((doc) => { 98 | expect(doc, 'Document should be provided').to.exist; 99 | expect(doc.events, 'Document events should be parsed').to.exist; 100 | 101 | expect(doc.events.length).to.equal(1); 102 | const event = doc.events[0]; 103 | 104 | expect(event, 'Event should be a valid entity').to.exist; 105 | expect(event.name).to.equal('click'); 106 | expect(event.visibility).to.equal('public'); 107 | expect(event.parent).to.be.equal('button'); 108 | expect(event.description).to.equal('Event fired when user clicked on button.'); 109 | 110 | done(); 111 | }).catch(e => { 112 | done(e); 113 | }); 114 | }); 115 | 116 | it('Markup events with modificators should be parsed', (done) => { 117 | parser.parse({ 118 | version: 2, 119 | filename: path.resolve(__dirname, 'event.markup.modificators.svelte'), 120 | features: ['events'], 121 | ignoredVisibilities: [] 122 | }).then((doc) => { 123 | expect(doc, 'Document should be provided').to.exist; 124 | expect(doc.events, 'Document events should be parsed').to.exist; 125 | 126 | expect(doc.events.length).to.equal(1); 127 | const event = doc.events[0]; 128 | 129 | expect(event, 'Event should be a valid entity').to.exist; 130 | expect(event.name).to.equal('click'); 131 | expect(event.visibility).to.equal('public'); 132 | expect(event.parent).to.be.equal('button'); 133 | expect(event.description).to.equal('Event fired when user clicked on button.'); 134 | 135 | expect(event.modificators).to.eql([ 136 | 'once', 'preventDefault' 137 | ]); 138 | 139 | done(); 140 | }).catch(e => { 141 | done(e); 142 | }); 143 | }); 144 | }); 145 | -------------------------------------------------------------------------------- /test/svelte2/integration/failure/failure.invalid.js.svelte: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/svelte2/integration/failure/failure.spec.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const chai = require('chai'); 3 | const expect = chai.expect; 4 | 5 | const parser = require('../../../../index'); 6 | 7 | describe('SvelteDoc - Failure', () => { 8 | it('Component have invalid JS syntax should fall into catch block', (done) => { 9 | parser.parse({ 10 | version: 2, 11 | filename: path.resolve(__dirname, 'failure.invalid.js.svelte'), 12 | ignoredVisibilities: [] 13 | }).then(() => { 14 | done(new Error('Should not be parsed')); 15 | }).catch(e => { 16 | expect(e).is.not.null; 17 | 18 | done(); 19 | }); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /test/svelte2/integration/locations/locations.spec.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const chai = require('chai'); 3 | const expect = chai.expect; 4 | 5 | const parser = require('../../../../index'); 6 | 7 | describe('SvelteDoc - Source locations', () => { 8 | it('Source locations for component data properties should be extracted', (done) => { 9 | parser.parse({ 10 | version: 2, 11 | includeSourceLocations: true, 12 | filename: path.resolve(__dirname, 'main.svelte'), 13 | features: ['data'], 14 | ignoredVisibilities: [] 15 | }).then((doc) => { 16 | expect(doc.data.length).is.equals(1); 17 | const property = doc.data[0]; 18 | 19 | expect(property.locations).to.deep.equal([{ 20 | start: 166, 21 | end: 176 22 | }]); 23 | 24 | done(); 25 | }).catch(e => { 26 | done(e); 27 | }); 28 | }); 29 | 30 | it('Source locations for component methods should be extracted', (done) => { 31 | parser.parse({ 32 | version: 2, 33 | includeSourceLocations: true, 34 | filename: path.resolve(__dirname, 'main.svelte'), 35 | features: ['methods'], 36 | ignoredVisibilities: [] 37 | }).then((doc) => { 38 | expect(doc.methods.length).is.equals(1); 39 | const method = doc.methods[0]; 40 | 41 | expect(method.locations).to.deep.equal([{ 42 | start: 221, 43 | end: 226 44 | }]); 45 | 46 | done(); 47 | }).catch(e => { 48 | done(e); 49 | }); 50 | }); 51 | 52 | it('Source locations for component refs should be extracted', (done) => { 53 | parser.parse({ 54 | version: 2, 55 | includeSourceLocations: true, 56 | filename: path.resolve(__dirname, 'main.svelte'), 57 | features: ['refs'], 58 | ignoredVisibilities: [] 59 | }).then((doc) => { 60 | expect(doc.refs.length).is.equals(1); 61 | const ref = doc.refs[0]; 62 | 63 | expect(ref.locations).to.deep.equal([{ 64 | start: 4, 65 | end: 15 66 | }]); 67 | 68 | done(); 69 | }).catch(e => { 70 | done(e); 71 | }); 72 | }); 73 | 74 | it('Source locations for component slots should be extracted', (done) => { 75 | parser.parse({ 76 | version: 2, 77 | includeSourceLocations: true, 78 | filename: path.resolve(__dirname, 'main.svelte'), 79 | features: ['slots'], 80 | ignoredVisibilities: [] 81 | }).then((doc) => { 82 | expect(doc.slots.length).is.equals(1); 83 | const slot = doc.slots[0]; 84 | 85 | expect(slot.locations).to.deep.equal([{ 86 | start: 64, 87 | end: 81 88 | }]); 89 | 90 | done(); 91 | }).catch(e => { 92 | done(e); 93 | }); 94 | }); 95 | 96 | it('Source locations for component events should be extracted', (done) => { 97 | parser.parse({ 98 | version: 2, 99 | includeSourceLocations: true, 100 | filename: path.resolve(__dirname, 'main.svelte'), 101 | features: ['events'], 102 | ignoredVisibilities: [] 103 | }).then((doc) => { 104 | expect(doc.events.length).is.equals(3); 105 | const markupPropogatedEvent = doc.events.find(e => e.name === 'mousemove'); 106 | 107 | expect(markupPropogatedEvent).is.not.empty; 108 | expect(markupPropogatedEvent.locations).to.deep.equal([{ 109 | start: 44, 110 | end: 58 111 | }]); 112 | 113 | const codeEvent = doc.events.find(e => e.name === 'codeEvent'); 114 | 115 | expect(codeEvent).is.not.empty; 116 | expect(codeEvent.locations).to.deep.equal([{ 117 | start: 287, 118 | end: 298 119 | }]); 120 | 121 | const markupEvent = doc.events.find(e => e.name === 'markupEvent'); 122 | 123 | expect(markupEvent).is.not.empty; 124 | expect(markupEvent.locations).to.deep.equal([{ 125 | start: 15, 126 | end: 44 127 | }]); 128 | 129 | done(); 130 | }).catch(e => { 131 | done(e); 132 | }); 133 | }); 134 | 135 | it('Source locations for component helpers should be extracted', (done) => { 136 | parser.parse({ 137 | version: 2, 138 | includeSourceLocations: true, 139 | filename: path.resolve(__dirname, 'main.svelte'), 140 | features: ['helpers'], 141 | ignoredVisibilities: [] 142 | }).then((doc) => { 143 | expect(doc.helpers.length).is.equals(2); 144 | const firstHelper = doc.helpers.find(h => h.name === 'window'); 145 | const secondHelper = doc.helpers.find(h => h.name === 'helperFunc'); 146 | 147 | expect(firstHelper.locations).to.deep.equal([{ 148 | start: 341, 149 | end: 347 150 | }]); 151 | 152 | expect(secondHelper.locations).to.deep.equal([{ 153 | start: 357, 154 | end: 367 155 | }]); 156 | 157 | done(); 158 | }).catch(e => { 159 | done(e); 160 | }); 161 | }); 162 | }); 163 | -------------------------------------------------------------------------------- /test/svelte2/integration/locations/main.svelte: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | 5 | -------------------------------------------------------------------------------- /test/svelte2/integration/overall/main.svelte: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | {pageHeader} 5 |
6 |
7 |
8 | 9 | {#each menuItems as item} 10 | 11 | {/each} 12 | 13 |
14 |
15 | 16 | 17 |
18 | 19 |
20 | 21 |
22 |
23 |
24 | 25 |
26 |
27 | 28 | 30 | 31 | -------------------------------------------------------------------------------- /test/svelte2/integration/overall/overall.main.spec.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const chai = require('chai'); 3 | const expect = chai.expect; 4 | 5 | const fs = require('fs'); 6 | 7 | const parser = require('../../../../index'); 8 | 9 | describe('SvelteDoc - Overall', () => { 10 | it('Component should be parsed with all features', (done) => { 11 | parser.parse({ 12 | version: 2, 13 | includeSourceLocations: true, 14 | filename: path.resolve(__dirname, 'main.svelte'), 15 | ignoredVisibilities: [] 16 | }).then((doc) => { 17 | expect(doc, 'Document should be provided').to.exist; 18 | 19 | // Remove function body expressions 20 | [ 21 | ...doc.methods, 22 | ...doc.actions, 23 | ...doc.transitions, 24 | ...doc.data 25 | ].forEach(m => { 26 | if (m.value && m.value.type) { 27 | delete m.value; 28 | } 29 | }); 30 | 31 | // fs.writeFileSync(path.resolve(__dirname, 'overall.main.doc.json'), JSON.stringify(doc, null, 4)); 32 | 33 | const expectedDoc = JSON.parse(fs.readFileSync(path.resolve(__dirname, 'overall.main.doc.json'))); 34 | 35 | expect(doc).to.deep.equal(expectedDoc); 36 | 37 | done(); 38 | }).catch(e => { 39 | done(e); 40 | }); 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /test/svelte2/integration/store/store.get.markup.svelte: -------------------------------------------------------------------------------- 1 |
2 |

{$ApplicationName}

3 |
4 | 5 | -------------------------------------------------------------------------------- /test/svelte2/integration/store/store.markup.svelte: -------------------------------------------------------------------------------- 1 |
2 |

{$ApplicationName}

3 |
4 |
5 | 8 |
9 | 10 | -------------------------------------------------------------------------------- /test/svelte2/integration/store/store.set.markup.svelte: -------------------------------------------------------------------------------- 1 |
2 | 5 |
6 | 7 | -------------------------------------------------------------------------------- /test/svelte2/integration/store/store.spec.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const chai = require('chai'); 3 | const expect = chai.expect; 4 | 5 | const parser = require('../../../../index'); 6 | 7 | xdescribe('SvelteDoc - Store', () => { 8 | it('Reading store properties in markup should be parsed', (done) => { 9 | parser.parse({ 10 | version: 2, 11 | filename: path.resolve(__dirname, 'store.get.markup.svelte'), 12 | features: ['store'], 13 | ignoredVisibilities: [] 14 | }).then((doc) => { 15 | expect(doc, 'Document should be provided').to.exist; 16 | expect(doc.store, 'Store information should be provided').to.exist; 17 | 18 | const storeDoc = doc.store; 19 | 20 | expect(storeDoc.length).to.equal(1); 21 | const storeProperty = storeDoc[0]; 22 | 23 | expect(storeProperty).to.exist; 24 | expect(storeProperty.name).to.equal('ApplicationName'); 25 | expect(storeProperty.read).is.true; 26 | expect(storeProperty.write).is.false; 27 | 28 | done(); 29 | }).catch(e => { 30 | done(e); 31 | }); 32 | }); 33 | 34 | it('Changing store properties in markup should be parsed', (done) => { 35 | parser.parse({ 36 | version: 2, 37 | filename: path.resolve(__dirname, 'store.set.markup.svelte'), 38 | features: ['store'], 39 | ignoredVisibilities: [] 40 | }).then((doc) => { 41 | expect(doc, 'Document should be provided').to.exist; 42 | expect(doc.store, 'Store information should be provided').to.exist; 43 | 44 | const storeDoc = doc.store; 45 | 46 | expect(storeDoc.length).to.equal(1); 47 | const storeProperty = storeDoc[0]; 48 | 49 | expect(storeProperty).to.exist; 50 | expect(storeProperty.name).to.equal('ApplicationName'); 51 | expect(storeProperty.read).is.false; 52 | expect(storeProperty.write).is.true; 53 | 54 | done(); 55 | }).catch(e => { 56 | done(e); 57 | }); 58 | }); 59 | 60 | it('Reading & changing store properties in markup should be parsed', (done) => { 61 | parser.parse({ 62 | version: 2, 63 | filename: path.resolve(__dirname, 'store.set.markup.svelte'), 64 | features: ['store'], 65 | ignoredVisibilities: [] 66 | }).then((doc) => { 67 | expect(doc, 'Document should be provided').to.exist; 68 | expect(doc.store, 'Store information should be provided').to.exist; 69 | 70 | const storeDoc = doc.store; 71 | 72 | expect(storeDoc.length).to.equal(1); 73 | const storeProperty = storeDoc[0]; 74 | 75 | expect(storeProperty).to.exist; 76 | expect(storeProperty.name).to.equal('ApplicationName'); 77 | expect(storeProperty.read).is.true; 78 | expect(storeProperty.write).is.true; 79 | 80 | done(); 81 | }).catch(e => { 82 | done(e); 83 | }); 84 | }); 85 | }); 86 | -------------------------------------------------------------------------------- /test/svelte3/integration/basic/basic.name.svelte: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KatChaotic/sveltedoc-parser/7b7d0ff777731ce8ed35ddead97eaa85afe9e82f/test/svelte3/integration/basic/basic.name.svelte -------------------------------------------------------------------------------- /test/svelte3/integration/basic/basic.name.wc.svelte: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /test/svelte3/integration/basic/basic.spec.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const chai = require('chai'); 3 | const expect = chai.expect; 4 | 5 | const parser = require('../../../../index'); 6 | 7 | describe('SvelteDoc v3 - Basic', () => { 8 | it('If name is not provided for component, name should be read from file name', (done) => { 9 | parser.parse({ 10 | version: 3, 11 | filename: path.resolve(__dirname, 'basic.name.svelte'), 12 | features: ['name'], 13 | ignoredVisibilities: [] 14 | }).then((doc) => { 15 | expect(doc, 'Document should be provided').to.exist; 16 | expect(doc.name, 'Document should have proper name').to.equal('BasicName'); 17 | 18 | done(); 19 | }).catch(e => { 20 | done(e); 21 | }); 22 | }); 23 | 24 | it('should read name from options-tag when compiling as a custom element', (done) => { 25 | parser.parse({ 26 | version: 3, 27 | filename: path.resolve(__dirname, 'basic.name.wc.svelte'), 28 | features: ['name'], 29 | ignoredVisibilities: [] 30 | }).then((doc) => { 31 | expect(doc, 'Document should be provided').to.exist; 32 | expect(doc.name, 'Document should have proper name').to.equal('tag-name'); 33 | 34 | done(); 35 | }).catch(e => { 36 | done(e); 37 | }); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /test/svelte3/integration/bind/bind.declared.svelte: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /test/svelte3/integration/bind/bind.exported.svelte: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /test/svelte3/integration/bind/bind.multiple.svelte: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /test/svelte3/integration/bind/bind.named.svelte: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /test/svelte3/integration/bind/bind.simple.svelte: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /test/svelte3/integration/bind/bind.spec.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const chai = require('chai'); 3 | const expect = chai.expect; 4 | 5 | const parser = require('../../../../index'); 6 | 7 | describe('SvelteDoc v3 - Bind', () => { 8 | it('Bind simple definition should be parsed', (done) => { 9 | parser.parse({ 10 | version: 3, 11 | filename: path.resolve(__dirname, 'bind.simple.svelte'), 12 | features: ['data'], 13 | ignoredVisibilities: [] 14 | }).then((doc) => { 15 | expect(doc, 'Document should be provided').to.exist; 16 | expect(doc.data, 'Document data should be parsed').to.exist; 17 | 18 | expect(doc.data.length).to.equal(1); 19 | const item = doc.data[0]; 20 | 21 | expect(item, 'Bind item should be a valid entity').to.exist; 22 | expect(item.name).to.equal('totalCost'); 23 | expect(item.visibility).to.equal('private'); 24 | expect(item.kind, 'Kind should not be specified, because variable is not declared').to.not.exist; 25 | expect(item.bind, 'Bind information should be presented').to.exist; 26 | expect(item.bind.length, 'Bind should be an array').to.be.equal(1); 27 | expect(item.bind[0].source).to.equal('ShopingCart'); 28 | expect(item.bind[0].property).to.equal('totalCost'); 29 | 30 | done(); 31 | }).catch(e => { 32 | done(e); 33 | }); 34 | }); 35 | 36 | it('Bind named definition should be parsed', (done) => { 37 | parser.parse({ 38 | version: 3, 39 | filename: path.resolve(__dirname, 'bind.named.svelte'), 40 | features: ['data'], 41 | ignoredVisibilities: [] 42 | }).then((doc) => { 43 | expect(doc, 'Document should be provided').to.exist; 44 | expect(doc.data, 'Document data should be parsed').to.exist; 45 | 46 | expect(doc.data.length, 'Found following data items - ' + doc.data.map(d => d.name).join(', ')).to.equal(1); 47 | const item = doc.data[0]; 48 | 49 | expect(item, 'Bind item should be a valid entity').to.exist; 50 | expect(item.name).to.equal('cost'); 51 | expect(item.visibility).to.equal('private'); 52 | expect(item.bind, 'Bind information should be presented').to.exist; 53 | expect(item.bind.length, 'Bind should be an array').to.be.equal(1); 54 | expect(item.bind[0].source).to.equal('ShopingCart'); 55 | expect(item.bind[0].property).to.equal('totalCost'); 56 | 57 | done(); 58 | }).catch(e => { 59 | done(e); 60 | }); 61 | }); 62 | 63 | it('Bind declared definition should be parsed', (done) => { 64 | parser.parse({ 65 | version: 3, 66 | filename: path.resolve(__dirname, 'bind.declared.svelte'), 67 | features: ['data'], 68 | ignoredVisibilities: [] 69 | }).then((doc) => { 70 | expect(doc, 'Document should be provided').to.exist; 71 | expect(doc.data, 'Document data should be parsed').to.exist; 72 | 73 | expect(doc.data.length).to.equal(1); 74 | const item = doc.data[0]; 75 | 76 | expect(item, 'Bind item should be a valid entity').to.exist; 77 | expect(item.name).to.equal('totalCost'); 78 | expect(item.visibility).to.equal('private'); 79 | expect(item.kind).to.equal('let'); 80 | expect(item.bind, 'Bind information should be presented').to.exist; 81 | expect(item.bind.length, 'Bind should be an array').to.be.equal(1); 82 | expect(item.bind[0].source).to.equal('ShopingCart'); 83 | expect(item.bind[0].property).to.equal('totalCost'); 84 | 85 | done(); 86 | }).catch(e => { 87 | done(e); 88 | }); 89 | }); 90 | 91 | it('Multiple bind to one variable should be parsed correctly', (done) => { 92 | parser.parse({ 93 | version: 3, 94 | filename: path.resolve(__dirname, 'bind.multiple.svelte'), 95 | features: ['data'], 96 | ignoredVisibilities: [] 97 | }).then((doc) => { 98 | expect(doc, 'Document should be provided').to.exist; 99 | expect(doc.data, 'Document data should be parsed').to.exist; 100 | 101 | expect(doc.data.length, 'Found following data items - ' + doc.data.map(d => d.name).join(', ')).to.equal(1); 102 | const item = doc.data[0]; 103 | 104 | expect(item, 'Bind item should be a valid entity').to.exist; 105 | expect(item.name).to.equal('number'); 106 | expect(item.visibility).to.equal('private'); 107 | expect(item.bind, 'Bind information should be presented').to.exist; 108 | expect(item.bind.length, 'Bind should be an array').to.be.equal(2); 109 | 110 | const bindInput = item.bind.find(b => b.source === 'input'); 111 | 112 | expect(bindInput.source).to.equal('input'); 113 | expect(bindInput.property).to.equal('value'); 114 | 115 | const bindControl = item.bind.find(b => b.source === 'PlusMinusControl'); 116 | 117 | expect(bindControl.source).to.equal('PlusMinusControl'); 118 | expect(bindControl.property).to.equal('numberValue'); 119 | 120 | done(); 121 | }).catch(e => { 122 | done(e); 123 | }); 124 | }); 125 | 126 | it('Bind declared and exported definition should be parsed', (done) => { 127 | parser.parse({ 128 | version: 3, 129 | filename: path.resolve(__dirname, 'bind.exported.svelte'), 130 | features: ['data', 'description'], 131 | ignoredVisibilities: [] 132 | }).then((doc) => { 133 | expect(doc, 'Document should be provided').to.exist; 134 | expect(doc.data, 'Document data should be parsed').to.exist; 135 | 136 | expect(doc.data.length).to.equal(1); 137 | const item = doc.data[0]; 138 | 139 | expect(item, 'Bind item should be a valid entity').to.exist; 140 | expect(item.name).to.equal('totalCost'); 141 | expect(item.visibility).to.equal('public'); 142 | expect(item.description).to.equal('The comment of public binded property.'); 143 | expect(item.bind, 'Bind information should be presented').to.exist; 144 | expect(item.bind.length, 'Bind should be an array').to.be.equal(1); 145 | expect(item.bind[0].source).to.equal('ShopingCart'); 146 | expect(item.bind[0].property).to.equal('totalCost'); 147 | 148 | done(); 149 | }).catch(e => { 150 | done(e); 151 | }); 152 | }); 153 | }); 154 | -------------------------------------------------------------------------------- /test/svelte3/integration/components/components.import.aliace.svelte: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/svelte3/integration/components/components.import.svelte: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/svelte3/integration/components/components.importStar.svelte: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/svelte3/integration/components/components.importable.js: -------------------------------------------------------------------------------- 1 | export const X = 1; 2 | 3 | export const y = 2; 4 | -------------------------------------------------------------------------------- /test/svelte3/integration/components/components.lowercase.svelte: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/svelte3/integration/components/components.nested.svelte: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/svelte3/integration/components/components.notdefault.svelte: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/svelte3/integration/components/components.spec.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const chai = require('chai'); 3 | const expect = chai.expect; 4 | 5 | const parser = require('../../../../index'); 6 | 7 | describe('SvelteDoc v3 - Components', () => { 8 | it('Import with upper case default should be parsed as component', (done) => { 9 | parser.parse({ 10 | version: 3, 11 | filename: path.resolve(__dirname, 'components.import.svelte'), 12 | features: ['components'], 13 | includeSourceLocations: true, 14 | ignoredVisibilities: [] 15 | }).then((doc) => { 16 | // Document 17 | expect(doc, 'Document should be provided').to.exist; 18 | expect(doc.components, 'Document components should be parsed').to.exist; 19 | expect(doc.components.length).to.equal(1); 20 | 21 | // Component 22 | const component = doc.components[0]; 23 | 24 | expect(component.name).to.equal('Nested'); 25 | expect(component.importPath).to.equal('./components.nested.svelte'); 26 | expect(component.visibility).to.equal('private'); 27 | expect(component.description).to.equal('The nested component.'); 28 | expect(component.locations, 'Code location should be included').to.be.exist; 29 | expect(component.locations.length).to.equal(1); 30 | 31 | // Location 32 | const location = component.locations[0]; 33 | 34 | expect(location, 'Location should be correctly identified').is.deep.equal({ start: 53, end: 59 }); 35 | 36 | done(); 37 | }).catch(e => { 38 | done(e); 39 | }); 40 | }); 41 | 42 | it('Import with upper case in aliace by default should not be parsed as component', (done) => { 43 | parser.parse({ 44 | version: 3, 45 | filename: path.resolve(__dirname, 'components.import.aliace.svelte'), 46 | features: ['components'], 47 | ignoredVisibilities: [] 48 | }).then((doc) => { 49 | expect(doc, 'Document should be provided').to.exist; 50 | expect(doc.components, 'Document components should be parsed').to.exist; 51 | 52 | expect(doc.components.length).to.equal(0); 53 | done(); 54 | }).catch(e => { 55 | done(e); 56 | }); 57 | }); 58 | 59 | it('Import with upper case not default should not be parsed as component', (done) => { 60 | parser.parse({ 61 | version: 3, 62 | filename: path.resolve(__dirname, 'components.notdefault.svelte'), 63 | features: ['components'], 64 | ignoredVisibilities: [] 65 | }).then((doc) => { 66 | expect(doc, 'Document should be provided').to.exist; 67 | expect(doc.components, 'Document components should be parsed').to.exist; 68 | 69 | expect(doc.components.length).to.equal(0); 70 | done(); 71 | }).catch(e => { 72 | done(e); 73 | }); 74 | }); 75 | 76 | it('Import with lowercase case default should not be parsed as component', (done) => { 77 | parser.parse({ 78 | version: 3, 79 | filename: path.resolve(__dirname, 'components.lowercase.svelte'), 80 | features: ['components'], 81 | ignoredVisibilities: [] 82 | }).then((doc) => { 83 | expect(doc, 'Document should be provided').to.exist; 84 | expect(doc.components, 'Document components should be parsed').to.exist; 85 | 86 | expect(doc.components.length).to.equal(0); 87 | done(); 88 | }).catch(e => { 89 | done(e); 90 | }); 91 | }); 92 | 93 | it('Import with start and alias should not be parsed as component', (done) => { 94 | parser.parse({ 95 | version: 3, 96 | filename: path.resolve(__dirname, 'components.importStar.svelte'), 97 | features: ['components'], 98 | ignoredVisibilities: [] 99 | }).then((doc) => { 100 | expect(doc, 'Document should be provided').to.exist; 101 | expect(doc.components, 'Document components should be parsed').to.exist; 102 | 103 | expect(doc.components.length).to.equal(0); 104 | done(); 105 | }).catch(e => { 106 | done(e); 107 | }); 108 | }); 109 | }); 110 | -------------------------------------------------------------------------------- /test/svelte3/integration/computed/computed.declaration.svelte: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/svelte3/integration/computed/computed.expression.svelte: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/svelte3/integration/computed/computed.spec.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const chai = require('chai'); 3 | const expect = chai.expect; 4 | 5 | const parser = require('../../../../index'); 6 | 7 | describe('SvelteDoc v3 - Computed', () => { 8 | it('Private computed without let should be parsed with comment and additional metadata', (done) => { 9 | parser.parse({ 10 | version: 3, 11 | filename: path.resolve(__dirname, 'computed.expression.svelte'), 12 | features: ['computed'], 13 | includeSourceLocations: true, 14 | ignoredVisibilities: [] 15 | }).then((doc) => { 16 | expect(doc, 'Document should be provided').to.exist; 17 | expect(doc.computed, 'Document computed should be parsed').to.exist; 18 | 19 | expect(doc.computed.length).to.equal(1); 20 | const prop = doc.computed[0]; 21 | 22 | expect(prop.name).to.equal('area'); 23 | expect(prop.visibility).to.equal('private'); 24 | expect(prop.static).to.be.false; 25 | 26 | expect(prop.description).to.equal('The area of rectangle.'); 27 | 28 | expect(prop.type).to.exist; 29 | expect(prop.type).to.eql({ kind: 'type', text: 'number', type: 'number' }); 30 | 31 | expect(prop.locations, 'Code location should be included').to.be.exist; 32 | expect(prop.locations.length).to.be.equal(1); 33 | 34 | const location = prop.locations[0]; 35 | 36 | expect(location, 'Location should be correct identified').is.deep.equals({ start: 124, end: 128 }); 37 | 38 | done(); 39 | }).catch(e => { 40 | done(e); 41 | }); 42 | }); 43 | 44 | it('Private computed should be parsed with comment and additional metadata', (done) => { 45 | parser.parse({ 46 | version: 3, 47 | filename: path.resolve(__dirname, 'computed.declaration.svelte'), 48 | features: ['computed'], 49 | ignoredVisibilities: [] 50 | }).then((doc) => { 51 | expect(doc, 'Document should be provided').to.exist; 52 | expect(doc.computed, 'Document computed should be parsed').to.exist; 53 | 54 | expect(doc.computed.length).to.equal(1); 55 | const prop = doc.computed[0]; 56 | 57 | expect(prop.name).to.equal('area'); 58 | expect(prop.visibility).to.equal('private'); 59 | expect(prop.static).to.be.false; 60 | 61 | expect(prop.description).to.equal('The area of rectangle.'); 62 | 63 | expect(prop.type).to.exist; 64 | expect(prop.type).to.eql({ kind: 'type', text: 'number', type: 'number' }); 65 | 66 | done(); 67 | }).catch(e => { 68 | done(e); 69 | }); 70 | }); 71 | }); 72 | -------------------------------------------------------------------------------- /test/svelte3/integration/data/const.importable.js: -------------------------------------------------------------------------------- 1 | const x = 1; 2 | 3 | export default x; 4 | 5 | export const y = 1; 6 | 7 | export const z = 1; 8 | -------------------------------------------------------------------------------- /test/svelte3/integration/data/data.const.svelte: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/svelte3/integration/data/data.export.aliace.svelte: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/svelte3/integration/data/data.export.functionExpression.svelte: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/svelte3/integration/data/data.export.many.svelte: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/svelte3/integration/data/data.exportNamed.many.svelte: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/svelte3/integration/data/data.import.aliace.svelte: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/svelte3/integration/data/data.import.default.svelte: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/svelte3/integration/data/data.import.many.svelte: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/svelte3/integration/data/data.multiple.svelte: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/svelte3/integration/data/data.objectPattern.svelte: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/svelte3/integration/data/data.private.svelte: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/svelte3/integration/data/data.public.svelte: -------------------------------------------------------------------------------- 1 | 10 | -------------------------------------------------------------------------------- /test/svelte3/integration/data/data.static.svelte: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/svelte3/integration/data/data.types.svelte: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/svelte3/integration/events/event.dispatcher.arrayIdentifier.svelte: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/svelte3/integration/events/event.dispatcher.custom.svelte: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/svelte3/integration/events/event.dispatcher.customConstructor.svelte: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/svelte3/integration/events/event.dispatcher.default.svelte: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/svelte3/integration/events/event.dispatcher.externalCallback.svelte: -------------------------------------------------------------------------------- 1 | 11 | 12 |
13 | Simple component 14 |
15 | 16 | -------------------------------------------------------------------------------- /test/svelte3/integration/events/event.dispatcher.identifier.svelte: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/svelte3/integration/events/event.dispatcher.importedIdentifier.svelte: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/svelte3/integration/events/event.dispatcher.insideMethod.svelte: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/svelte3/integration/events/event.dispatcher.markup.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 | -------------------------------------------------------------------------------- /test/svelte3/integration/events/event.markup.dispatcher.custom.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 | -------------------------------------------------------------------------------- /test/svelte3/integration/events/event.markup.dispatcher.customConstructor.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /test/svelte3/integration/events/event.markup.dispatcher.default.function.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /test/svelte3/integration/events/event.markup.dispatcher.default.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 | -------------------------------------------------------------------------------- /test/svelte3/integration/events/event.markup.handleAndPropogate.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 |
8 | 9 | 12 |
-------------------------------------------------------------------------------- /test/svelte3/integration/events/event.markup.modificators.svelte: -------------------------------------------------------------------------------- 1 |
2 | 3 | 6 |
-------------------------------------------------------------------------------- /test/svelte3/integration/events/event.markup.propogate.svelte: -------------------------------------------------------------------------------- 1 |
2 | 3 | 6 |
-------------------------------------------------------------------------------- /test/svelte3/integration/events/sharedEvents.importable.js: -------------------------------------------------------------------------------- 1 | export const EVENT = { 2 | SIGNAL: { 3 | NOTIFY: 'notify', 4 | }, 5 | }; 6 | 7 | export const SIMPLE_EVENT = 'plain-notify'; 8 | -------------------------------------------------------------------------------- /test/svelte3/integration/globalComment/globalComment.markup.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 | 12 | 13 | 21 |
22 |

Header {identifier}

23 |
24 | -------------------------------------------------------------------------------- /test/svelte3/integration/globalComment/globalComment.nested.svelte: -------------------------------------------------------------------------------- 1 | 17 | 18 |
19 |
20 | 28 |
29 |
30 |
31 |

Header {identifier}

32 |
33 | -------------------------------------------------------------------------------- /test/svelte3/integration/globalComment/globalComment.noComment.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 |
6 |

Header {identifier}

7 |
8 | -------------------------------------------------------------------------------- /test/svelte3/integration/globalComment/globalComment.script.svelte: -------------------------------------------------------------------------------- 1 | 14 | -------------------------------------------------------------------------------- /test/svelte3/integration/globalComment/globalComment.spec.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const chai = require('chai'); 3 | const expect = chai.expect; 4 | 5 | const parser = require('../../../../index'); 6 | 7 | describe('SvelteDoc v3 - Global component', () => { 8 | it('Global component data should be parsed with HTML comment', (done) => { 9 | parser.parse({ 10 | version: 3, 11 | filename: path.resolve(__dirname, 'globalComment.markup.svelte'), 12 | features: ['description', 'keywords'], 13 | includeSourceLocations: true, 14 | ignoredVisibilities: [] 15 | }).then((doc) => { 16 | expect(doc, 'Document should be provided').to.exist; 17 | expect(doc.description, 'Document description should be parsed').to.exist; 18 | expect(doc.keywords, 'Document keywords should be parsed').to.exist; 19 | 20 | expect(doc.description, 'Document description text').to.equal('The component description'); 21 | expect(doc.keywords, 'Document keywords length').to.have.length(2); 22 | expect(doc.keywords, 'Document keywords content').to.eql([ 23 | { name: 'component', description: 'ComponentName' }, 24 | { name: 'example', description: '' } 25 | ]); 26 | done(); 27 | }).catch(done); 28 | }); 29 | 30 | it('Global component data should be parsed with JS comment', (done) => { 31 | parser.parse({ 32 | version: 3, 33 | filename: path.resolve(__dirname, 'globalComment.script.svelte'), 34 | features: ['description', 'keywords'], 35 | includeSourceLocations: true, 36 | ignoredVisibilities: [] 37 | }).then((doc) => { 38 | expect(doc, 'Document should be provided').to.exist; 39 | expect(doc.description, 'Document description should be parsed').to.exist; 40 | expect(doc.keywords, 'Document keywords should be parsed').to.exist; 41 | 42 | expect(doc.description, 'Document description text').to.equal('The component description'); 43 | expect(doc.keywords, 'Document keywords length').to.have.length(2); 44 | expect(doc.keywords, 'Document keywords content').to.eql([ 45 | { name: 'component', description: 'ComponentName' }, 46 | { name: 'example', description: '' } 47 | ]); 48 | done(); 49 | }).catch(done); 50 | }); 51 | 52 | it('Global component data should NOT be parsed when not top level', (done) => { 53 | parser.parse({ 54 | version: 3, 55 | filename: path.resolve(__dirname, 'globalComment.nested.svelte'), 56 | features: ['description', 'keywords'], 57 | includeSourceLocations: true, 58 | ignoredVisibilities: [] 59 | }).then((doc) => { 60 | expect(doc, 'Document').to.exist; 61 | expect(doc.description, 'Document description').to.not.exist; 62 | expect(doc.keywords, 'Document keywords').to.be.empty; 63 | 64 | done(); 65 | }).catch(done); 66 | }); 67 | 68 | it('Global component data should NOT be parsed when no comment exist', (done) => { 69 | parser.parse({ 70 | version: 3, 71 | filename: path.resolve(__dirname, 'globalComment.noComment.svelte'), 72 | features: ['description', 'keywords'], 73 | includeSourceLocations: true, 74 | ignoredVisibilities: [] 75 | }).then((doc) => { 76 | expect(doc, 'Document').to.exist; 77 | expect(doc.description, 'Document description').to.not.exist; 78 | expect(doc.keywords, 'Document keywords').to.be.empty; 79 | 80 | done(); 81 | }).catch(done); 82 | }); 83 | }); 84 | -------------------------------------------------------------------------------- /test/svelte3/integration/locations/locations.multiscripts.svelte: -------------------------------------------------------------------------------- 1 |

Template header

2 |

Template text

3 | 4 | 7 | 8 | -------------------------------------------------------------------------------- /test/svelte3/integration/locations/locations.spec.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const chai = require('chai'); 3 | const expect = chai.expect; 4 | 5 | const parser = require('../../../../index'); 6 | 7 | function assertDataItemLocation(dataItem, expectedLocationStart, expectedLocationEnd) { 8 | expect(dataItem.locations, `Code location for data item should be included for "${dataItem.name}"`).to.be.exist; 9 | expect(dataItem.locations.length, `Code location for data item have values for "${dataItem.name}"`).to.be.equal(1); 10 | const location = dataItem.locations[0]; 11 | 12 | expect(location, `Location should be correct identified for "${dataItem.name}"`).is.deep.equals({ start: expectedLocationStart, end: expectedLocationEnd }); 13 | } 14 | 15 | describe('SvelteDoc v3 - Locations', () => { 16 | it('Locations for multiple scripts should be found correct', (done) => { 17 | parser.parse({ 18 | version: 3, 19 | filename: path.resolve(__dirname, 'locations.multiscripts.svelte'), 20 | features: ['data'], 21 | includeSourceLocations: true, 22 | ignoredVisibilities: [] 23 | }).then((doc) => { 24 | expect(doc, 'Document should be provided').to.exist; 25 | expect(doc.data, 'Document events should be parsed').to.exist; 26 | 27 | const staticVariable = doc.data.find(p => p.name === 'staticVariable'); 28 | 29 | expect(staticVariable, '"staticVariable" should be presented in data items of the doc').to.exist; 30 | expect(staticVariable.static).to.be.true; 31 | assertDataItemLocation(staticVariable, 81, 95); 32 | 33 | const local = doc.data.find(p => p.name === 'variable'); 34 | 35 | expect(local, '"variable" should be presented in data items of the doc').to.exist; 36 | expect(local.static).to.be.false; 37 | assertDataItemLocation(local, 129, 137); 38 | 39 | done(); 40 | }).catch(e => { 41 | done(e); 42 | }); 43 | }); 44 | }); 45 | -------------------------------------------------------------------------------- /test/svelte3/integration/methods/method.private.svelte: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/svelte3/integration/methods/method.public.svelte: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/svelte3/integration/methods/method.typeInference.svelte: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/svelte3/integration/methods/methods.spec.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const chai = require('chai'); 3 | const expect = chai.expect; 4 | 5 | const parser = require('../../../../index'); 6 | 7 | describe('SvelteDoc v3 - Methods', () => { 8 | it('Private method should be parsed with comment and additional metadata', (done) => { 9 | parser.parse({ 10 | version: 3, 11 | filename: path.resolve(__dirname, 'method.private.svelte'), 12 | features: ['methods'], 13 | includeSourceLocations: true, 14 | ignoredVisibilities: [] 15 | }).then((doc) => { 16 | expect(doc, 'Document should be provided').to.exist; 17 | expect(doc.methods, 'Document methods should be parsed').to.exist; 18 | 19 | expect(doc.methods.length).to.equal(1); 20 | const method = doc.methods[0]; 21 | 22 | expect(method.name).to.equal('privateMethod'); 23 | expect(method.visibility).to.equal('private'); 24 | expect(method.static).to.be.false; 25 | 26 | expect(method.params, 'Method arguments should be parsed').to.exist; 27 | expect(method.params.length).to.equal(2); 28 | expect(method.params[0].name).to.equal('param1'); 29 | expect(method.params[1].name).to.equal('param2'); 30 | 31 | expect(method.description).to.equal('The method comment.'); 32 | 33 | expect(method.return, 'Method return keyword should be parsed').to.exist; 34 | expect(method.return.type).to.exist; 35 | expect(method.return.type.type).to.equal('number'); 36 | expect(method.return.description).to.equal('return value description'); 37 | 38 | expect(method.locations, 'Code location should be included').to.be.exist; 39 | expect(method.locations.length).to.be.equal(1); 40 | 41 | const location = method.locations[0]; 42 | 43 | expect(location, 'Location should be correct identified').is.deep.equals({ start: 117, end: 130 }); 44 | 45 | done(); 46 | }).catch(e => { 47 | done(e); 48 | }); 49 | }); 50 | 51 | it('Public method should be parsed with comment and additional metadata', (done) => { 52 | parser.parse({ 53 | version: 3, 54 | filename: path.resolve(__dirname, 'method.public.svelte'), 55 | features: ['methods', 'description'], 56 | ignoredVisibilities: [] 57 | }).then((doc) => { 58 | expect(doc, 'Document should be provided').to.exist; 59 | 60 | expect(doc.methods, 'Document methods should be parsed').to.exist; 61 | expect(doc.methods.length).to.equal(1); 62 | 63 | const method = doc.methods[0]; 64 | 65 | expect(method.name).to.equal('publicMethod'); 66 | expect(method.visibility).to.equal('public'); 67 | expect(method.static).to.be.false; 68 | expect(method.description).to.equal('The method comment.'); 69 | 70 | // @param keywords 71 | expect(method.params, 'Method arguments should be parsed').to.exist; 72 | expect(method.params.length).to.equal(2); 73 | 74 | const param0 = method.params[0]; 75 | 76 | expect(param0.name).to.equal('param1'); 77 | expect(param0.description).to.equal('the first parameter'); 78 | expect(param0.optional).to.be.false; 79 | 80 | const param1 = method.params[1]; 81 | 82 | expect(param1.name).to.equal('param2'); 83 | expect(param1.description).to.equal('the second parameter'); 84 | expect(param1.optional).to.be.true; 85 | 86 | // @returns keyword 87 | expect(method.return, 'Method return keyword should be parsed').to.exist; 88 | expect(method.return.type).to.exist; 89 | expect(method.return.type.type).to.equal('number'); 90 | expect(method.return.description).to.equal('return value description'); 91 | 92 | done(); 93 | }).catch(e => { 94 | done(e); 95 | }); 96 | }); 97 | 98 | it('Method argument types should be inferred from assignment patterns, and enhanced with additional metadata', (done) => { 99 | parser.parse({ 100 | version: 3, 101 | filename: path.resolve(__dirname, 'method.typeInference.svelte'), 102 | features: ['methods', 'description'], 103 | ignoredVisibilities: [] 104 | }).then((doc) => { 105 | expect(doc, 'Document should be provided').to.exist; 106 | 107 | expect(doc.methods, 'Document methods should be parsed').to.exist; 108 | expect(doc.methods.length).to.equal(1); 109 | 110 | const method = doc.methods[0]; 111 | 112 | expect(method.name).to.equal('publicMethod'); 113 | expect(method.visibility).to.equal('public'); 114 | expect(method.static).to.be.false; 115 | expect(method.description).to.equal('The method comment.'); 116 | 117 | // @param keywords 118 | expect(method.params, 'Method arguments should be parsed').to.exist; 119 | expect(method.params.length).to.equal(7); 120 | 121 | const param0 = method.params[0]; 122 | 123 | expect(param0.name).to.equal('param1'); 124 | expect(param0.description).to.equal('the first parameter'); 125 | expect(param0.type.type).to.equal('string'); 126 | expect(param0.defaultValue).to.be.undefined; 127 | expect(param0.optional).to.be.false; 128 | 129 | const param1 = method.params[1]; 130 | 131 | expect(param1.name).to.equal('param2'); 132 | expect(param1.description).to.equal('the second parameter'); 133 | expect(param1.type.type).to.equal('boolean'); 134 | expect(param1.defaultValue).to.equal('false'); 135 | expect(param1.optional).to.be.true; 136 | 137 | const param2 = method.params[2]; 138 | 139 | expect(param2.name).to.equal('param3'); 140 | expect(param2.description).to.equal('the third parameter of type number'); 141 | expect(param2.type.type).to.equal('number'); 142 | expect(param2.defaultValue).to.equal('1'); 143 | expect(param2.optional).to.be.true; 144 | 145 | const param3 = method.params[3]; 146 | 147 | expect(param3.name).to.equal('param4'); 148 | expect(param3.description).to.be.null; 149 | expect(param3.type.type).to.equal('array'); 150 | expect(param3.defaultValue).to.be.undefined; 151 | expect(param3.optional).to.be.true; 152 | 153 | const param4 = method.params[4]; 154 | 155 | expect(param4.name).to.equal('param5'); 156 | expect(param4.description).to.be.null; 157 | expect(param4.type.type).to.equal('object'); 158 | expect(param4.defaultValue).to.be.undefined; 159 | expect(param4.optional).to.be.true; 160 | 161 | const param5 = method.params[5]; 162 | 163 | expect(param5.name).to.equal('param6'); 164 | expect(param5.description).to.be.null; 165 | expect(param5.type.type).to.equal('boolean'); 166 | expect(param5.defaultValue).to.equal('true'); 167 | expect(param5.optional).to.be.true; 168 | 169 | const param6 = method.params[6]; 170 | 171 | expect(param6.name).to.equal('param7'); 172 | expect(param6.description).to.be.null; 173 | expect(param6.type.type).to.equal('any'); 174 | expect(param6.defaultValue).to.be.undefined; 175 | expect(param6.optional).to.be.false; 176 | 177 | // @returns keyword 178 | expect(method.return, 'Method return keyword should be parsed').to.exist; 179 | expect(method.return.type).to.exist; 180 | expect(method.return.type.type).to.equal('number'); 181 | expect(method.return.description).to.equal('return value description'); 182 | 183 | done(); 184 | }).catch(e => { 185 | done(e); 186 | }); 187 | }); 188 | }); 189 | -------------------------------------------------------------------------------- /test/svelte3/integration/refs/ref.component.svelte: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /test/svelte3/integration/refs/ref.declared.svelte: -------------------------------------------------------------------------------- 1 |
2 | Header 3 |
4 | 5 | -------------------------------------------------------------------------------- /test/svelte3/integration/refs/ref.simple.svelte: -------------------------------------------------------------------------------- 1 |
2 | The header 3 |
-------------------------------------------------------------------------------- /test/svelte3/integration/refs/refs.spec.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const chai = require('chai'); 3 | const expect = chai.expect; 4 | 5 | const parser = require('../../../../index'); 6 | 7 | describe('SvelteDoc v3 - Refs', () => { 8 | it('Ref to non declared variable', (done) => { 9 | parser.parse({ 10 | version: 3, 11 | filename: path.resolve(__dirname, 'ref.simple.svelte'), 12 | features: ['refs'], 13 | ignoredVisibilities: [] 14 | }).then((doc) => { 15 | expect(doc, 'Document should be provided').to.exist; 16 | expect(doc.refs, 'Document refs should be parsed').to.exist; 17 | 18 | expect(doc.refs.length).to.equal(1); 19 | const ref = doc.refs[0]; 20 | 21 | expect(ref, 'Ref should be a valid entity').to.exist; 22 | expect(ref.name).to.equal('header'); 23 | expect(ref.parent).to.equal('div'); 24 | expect(ref.visibility).to.equal('private'); 25 | 26 | done(); 27 | }).catch(e => { 28 | done(e); 29 | }); 30 | }); 31 | 32 | it('Ref to declared variable', (done) => { 33 | parser.parse({ 34 | version: 3, 35 | filename: path.resolve(__dirname, 'ref.declared.svelte'), 36 | features: ['refs'], 37 | ignoredVisibilities: [] 38 | }).then((doc) => { 39 | expect(doc, 'Document should be provided').to.exist; 40 | expect(doc.refs, 'Document refs should be parsed').to.exist; 41 | 42 | expect(doc.refs.length).to.equal(1); 43 | const ref = doc.refs[0]; 44 | 45 | expect(ref, 'Ref should be a valid entity').to.exist; 46 | expect(ref.name).to.equal('header'); 47 | expect(ref.parent).to.equal('div'); 48 | expect(ref.visibility).to.equal('private'); 49 | 50 | done(); 51 | }).catch(e => { 52 | done(e); 53 | }); 54 | }); 55 | 56 | it('Ref to component', (done) => { 57 | parser.parse({ 58 | version: 3, 59 | filename: path.resolve(__dirname, 'ref.component.svelte'), 60 | features: ['refs'], 61 | ignoredVisibilities: [] 62 | }).then((doc) => { 63 | expect(doc, 'Document should be provided').to.exist; 64 | expect(doc.refs, 'Document refs should be parsed').to.exist; 65 | 66 | expect(doc.refs.length).to.equal(1); 67 | const ref = doc.refs[0]; 68 | 69 | expect(ref, 'Ref should be a valid entity').to.exist; 70 | expect(ref.name).to.equal('cart'); 71 | expect(ref.parent).to.equal('ShopingCart'); 72 | expect(ref.visibility).to.equal('private'); 73 | 74 | done(); 75 | }).catch(e => { 76 | done(e); 77 | }); 78 | }); 79 | }); 80 | -------------------------------------------------------------------------------- /test/svelte3/integration/slots/slot.comments.svelte: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 |
5 | 6 | 7 |
9 | 10 | 11 |
-------------------------------------------------------------------------------- /test/svelte3/integration/slots/slot.default.svelte: -------------------------------------------------------------------------------- 1 |
2 | 3 | Default slot 4 | 5 |
-------------------------------------------------------------------------------- /test/svelte3/integration/slots/slot.named.svelte: -------------------------------------------------------------------------------- 1 |
2 | 3 | Default title 4 | 5 | 6 | Default body content 7 | 8 |
-------------------------------------------------------------------------------- /test/svelte3/integration/slots/slot.parameters.svelte: -------------------------------------------------------------------------------- 1 |
    2 | {#each items as item} 3 |
  • 4 | 8 | 9 |
  • 10 | {/each} 11 |
12 | 13 | -------------------------------------------------------------------------------- /test/svelte3/integration/slots/slots.spec.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const chai = require('chai'); 3 | const expect = chai.expect; 4 | 5 | const packageConfig = require('../../../../package.json'); 6 | const parser = require('../../../../index'); 7 | 8 | describe('SvelteDoc v3 - Slots', () => { 9 | it('Default slot should be found', (done) => { 10 | parser.parse({ 11 | version: 3, 12 | filename: path.resolve(__dirname, 'slot.default.svelte'), 13 | features: ['slots'], 14 | ignoredVisibilities: [] 15 | }).then((doc) => { 16 | expect(doc, 'Document should be provided').to.exist; 17 | expect(doc.slots, 'Document slots should be parsed').to.exist; 18 | 19 | expect(doc.slots.length).to.equal(1); 20 | const slot = doc.slots[0]; 21 | 22 | expect(slot, 'Slot should be a valid entity').to.exist; 23 | expect(slot.name).to.equal('default'); 24 | expect(slot.visibility).to.equal('public'); 25 | 26 | done(); 27 | }).catch(e => { 28 | done(e); 29 | }); 30 | }); 31 | 32 | it('Slot parameters should be exposed', (done) => { 33 | parser.parse({ 34 | version: 3, 35 | filename: path.resolve(__dirname, 'slot.parameters.svelte'), 36 | features: ['slots'], 37 | ignoredVisibilities: [] 38 | }).then((doc) => { 39 | expect(doc, 'Document should be provided').to.exist; 40 | expect(doc.slots, 'Document slots should be parsed').to.exist; 41 | 42 | expect(doc.slots.length).to.equal(1); 43 | const slot = doc.slots[0]; 44 | 45 | expect(slot, 'Slot should be a valid entity').to.exist; 46 | expect(slot.name).to.equal('item'); 47 | expect(slot.visibility).to.equal('public'); 48 | 49 | const parameters = slot.params; 50 | 51 | expect(parameters).to.exist; 52 | expect(parameters.length).to.equal(1); 53 | 54 | const parameter = parameters[0]; 55 | 56 | expect(parameter).to.exist; 57 | expect(parameter.name).to.equal('item'); 58 | expect(parameter.description).to.equal('The slot parameter description.'); 59 | expect(parameter.type).to.deep.eq({ 60 | kind: 'type', 61 | type: 'string', 62 | text: 'string' 63 | }); 64 | 65 | // TODO: 5.* | Backward compatibility test 66 | if (packageConfig.version.startsWith('5.')) { 67 | expect(slot.parameters, 'parameters field should be removed').undefined; 68 | } else { 69 | expect(slot.parameters, 'Should be backward compatable until 5.* version').to.deep.eq(slot.params); 70 | } 71 | 72 | done(); 73 | }).catch(e => { 74 | done(e); 75 | }); 76 | }); 77 | 78 | it('Named slots should be found', (done) => { 79 | parser.parse({ 80 | version: 3, 81 | filename: path.resolve(__dirname, 'slot.named.svelte'), 82 | features: ['slots'], 83 | ignoredVisibilities: [] 84 | }).then((doc) => { 85 | expect(doc, 'Document should be provided').to.exist; 86 | expect(doc.slots, 'Document slots should be parsed').to.exist; 87 | 88 | expect(doc.slots.length).to.equal(2); 89 | 90 | expect(doc.slots[0], 'Slot should be a valid entity').to.exist; 91 | expect(doc.slots[0].name).to.equal('title'); 92 | expect(doc.slots[0].visibility).to.equal('public'); 93 | 94 | expect(doc.slots[1], 'Slot should be a valid entity').to.exist; 95 | expect(doc.slots[1].name).to.equal('body'); 96 | expect(doc.slots[1].visibility).to.equal('public'); 97 | 98 | done(); 99 | }).catch(e => { 100 | done(e); 101 | }); 102 | }); 103 | 104 | it('Slot comments should be correctly parsed', done => { 105 | parser.parse({ 106 | version: 3, 107 | filename: path.resolve(__dirname, 'slot.comments.svelte'), 108 | features: ['slots', 'events'], 109 | ignoredVisibilities: [] 110 | }).then(doc => { 111 | expect(doc, 'Document should be provided').to.exist; 112 | expect(doc.slots, 'Document slots should be parsed').to.exist; 113 | 114 | expect(doc.slots).to.have.length(3); 115 | 116 | const firstSlot = doc.slots.find(slot => slot.name === 'first'); 117 | const defaultSlot = doc.slots.find(slot => slot.name === 'default'); 118 | const secondSlot = doc.slots.find(slot => slot.name === 'second'); 119 | 120 | expect(firstSlot.description).to.equal('The first slot description.'); 121 | expect(defaultSlot.description).to.empty; 122 | expect(secondSlot.description).to.equal('The second slot description.'); 123 | 124 | expect(doc.events).to.have.length(1); 125 | 126 | const event = doc.events[0]; 127 | 128 | expect(event.description).to.be.empty; 129 | 130 | done(); 131 | }).catch(e => { 132 | done(e); 133 | }); 134 | }); 135 | }); 136 | -------------------------------------------------------------------------------- /test/unit/helpers/helpers.spec.js: -------------------------------------------------------------------------------- 1 | const chai = require('chai'); 2 | const expect = chai.expect; 3 | 4 | const helpers = require('../../../lib/helpers'); 5 | 6 | describe('Helpers parser module tests', () => { 7 | describe('helpers.extractHtmlBlock', () => { 8 | it('When html block exists should be provide a correct result', () => { 9 | const content = ` 10 |

Some content here

11 | 14 | `; 15 | 16 | const result = helpers.extractHtmlBlock(content, 'script'); 17 | 18 | expect(result).is.exist; 19 | const block = result.block; 20 | 21 | expect(block).is.exist; 22 | expect(block.content).is.eq('\nlet variable = 1;\n'); 23 | expect(block.offset).is.eq(34); 24 | expect(block.outerPosition).is.exist; 25 | expect(block.outerPosition.start).is.eq(26); 26 | expect(block.outerPosition.end).is.eq(62); 27 | expect(block.attributes).is.empty; 28 | }); 29 | 30 | it('When html block have attribute, should be correct extracted', () => { 31 | const content = ` 32 |

Some content here

33 | 36 | `; 37 | 38 | const result = helpers.extractHtmlBlock(content, 'script'); 39 | 40 | expect(result).is.exist; 41 | const block = result.block; 42 | 43 | expect(block).is.exist; 44 | expect(block.attributes).is.eq(' context="module"'); 45 | }); 46 | 47 | it('When startIndex are specified, should be skip first content and provide the next item', () => { 48 | const content = ` 49 |

Some content here

50 | 53 | 56 | `; 57 | 58 | const result = helpers.extractHtmlBlock(content, 'script', 62); 59 | 60 | expect(result).is.exist; 61 | const block = result.block; 62 | 63 | expect(block).is.exist; 64 | expect(block.content).is.eq('\nlet variable = 2;\n'); 65 | expect(block.offset).is.eq(71); 66 | expect(block.outerPosition).is.exist; 67 | expect(block.outerPosition.start).is.eq(63); 68 | expect(block.outerPosition.end).is.eq(99); 69 | expect(block.attributes).is.empty; 70 | }); 71 | }); 72 | 73 | describe('helpers.extractAllHtmlBlocks', () => { 74 | it('Extract one script block', () => { 75 | const content = ` 76 |

Some content here

77 | 80 | `; 81 | const result = helpers.extractAllHtmlBlocks(content, 'script'); 82 | 83 | expect(result).is.exist; 84 | expect(result.blocks).is.exist; 85 | expect(result.blocks.length).is.eq(1); 86 | 87 | const block = result.blocks[0]; 88 | 89 | expect(block).is.exist; 90 | expect(block.offset).is.eq(34); 91 | expect(block.content).is.eq('\nlet variable = 1;\n'); 92 | expect(block.attributes).is.empty; 93 | }); 94 | 95 | it('Extract two script blocks', () => { 96 | const content = ` 97 |

Some content here

98 | 101 | 104 | `; 105 | const result = helpers.extractAllHtmlBlocks(content, 'script'); 106 | 107 | expect(result).is.exist; 108 | expect(result.blocks).is.exist; 109 | expect(result.blocks.length).is.eq(2); 110 | 111 | let block = result.blocks[0]; 112 | 113 | expect(block).is.exist; 114 | expect(block.offset).is.eq(34); 115 | expect(block.content).is.eq('\nlet variable = 1;\n'); 116 | expect(block.attributes).is.empty; 117 | 118 | block = result.blocks[1]; 119 | expect(block).is.exist; 120 | expect(block.offset).is.eq(71); 121 | expect(block.content).is.eq('\nlet variable = 2;\n'); 122 | expect(block.attributes).is.empty; 123 | }); 124 | 125 | // TODO Re-enable for following issue: https://github.com/alexprey/sveltedoc-parser/issues/58 126 | xit('When html block contains complex markup with escaping and string usages should provide a correct result', () => { 127 | const content = ` 128 | 131 | 132 | 133 |

134 | Active tab: {activeTab1[0].title} 135 |
136 |
137 | 138 | 139 | {@html highlight( 140 | ' \n\n' + 148 | '')} 149 | 150 |
151 | `; 152 | 153 | const result = helpers.extractAllHtmlBlocks(content, 'script'); 154 | 155 | expect(result).is.exist; 156 | expect(result.blocks).is.exist; 157 | expect(result.blocks.length).is.eq(1); 158 | 159 | const block = result.blocks[0]; 160 | 161 | expect(block).is.exist; 162 | expect(block.offset).is.eq(9); 163 | expect(block.content).is.eq('\nexport let items = [];\n'); 164 | expect(block.attributes).is.empty; 165 | }); 166 | }); 167 | }); 168 | -------------------------------------------------------------------------------- /test/unit/helpers/utils.spec.js: -------------------------------------------------------------------------------- 1 | const utils = require('../../../lib/utils'); 2 | 3 | const espree = require('espree'); 4 | const { expect } = require('chai'); 5 | 6 | describe('"utils.js" module', () => { 7 | describe('"buildCamelCase" method', () => { 8 | it('when input is already camel cased then should return same value', () => { 9 | const result = utils.buildCamelCase('CamelCasedTestMethodName12'); 10 | 11 | expect(result).to.equal('CamelCasedTestMethodName12'); 12 | }); 13 | 14 | it('when spaces used in name then should remove them and make next char uppercased', () => { 15 | const result = utils.buildCamelCase('Spaces In the name'); 16 | 17 | expect(result).to.equal('SpacesInTheName'); 18 | }); 19 | 20 | it('when first letter is lowercased then should be changed to upper case', () => { 21 | const result = utils.buildCamelCase('lowercasedFirstLetter'); 22 | 23 | expect(result).to.equal('LowercasedFirstLetter'); 24 | }); 25 | 26 | it('when illegal chars in name then should remove then and make next char uppercased', () => { 27 | const result = utils.buildCamelCase('Illegal-chars-In-the-name'); 28 | 29 | expect(result).to.equal('IllegalCharsInTheName'); 30 | }); 31 | }); 32 | 33 | describe('buildPropertyAccessorChainFromTokens', () => { 34 | describe('should return an accessor chain when', () => { 35 | it('tokens represent a single identifier', () => { 36 | const expectedChain = ['NOTIFY']; 37 | const script = ` 38 | callee(${expectedChain.join('.')}); 39 | `; 40 | const tokens = espree.tokenize(script); 41 | const identifierTokens = tokens.slice(2); 42 | 43 | const chain = utils.buildPropertyAccessorChainFromTokens(identifierTokens); 44 | 45 | expect(chain).to.deep.equal(expectedChain); 46 | }); 47 | 48 | it('tokens represent chained identifiers using dot notation', () => { 49 | const expectedChain = ['EVENT', 'SIGNAL', 'NOTIFY']; 50 | const script = ` 51 | callee(${expectedChain.join('.')}); 52 | `; 53 | const tokens = espree.tokenize(script); 54 | const identifierTokens = tokens.slice(2); 55 | 56 | const chain = utils.buildPropertyAccessorChainFromTokens(identifierTokens); 57 | 58 | expect(chain).to.deep.equal(expectedChain); 59 | }); 60 | 61 | it('tokens represent chained identifiers using mixed notation', () => { 62 | const expectedChain = ['EVENT', 'SIGNAL', 'NOTIFY']; 63 | const script = ` 64 | callee(${expectedChain[0]}['${expectedChain[1]}'].${expectedChain[2]}); 65 | `; 66 | const tokens = espree.tokenize(script); 67 | const identifierTokens = tokens.slice(2); 68 | 69 | const chain = utils.buildPropertyAccessorChainFromTokens(identifierTokens); 70 | 71 | expect(chain).to.deep.equal(expectedChain); 72 | }); 73 | it('tokens represent chained identifiers using only bracket notation', () => { 74 | const expectedChain = ['EVENT', '"SIGNAL"', "'NOTIFY'"]; 75 | const script = ` 76 | callee(${expectedChain[0]}['${expectedChain[1]}']["${expectedChain[2]}"]); 77 | `; 78 | const tokens = espree.tokenize(script); 79 | const identifierTokens = tokens.slice(2); 80 | 81 | const chain = utils.buildPropertyAccessorChainFromTokens(identifierTokens); 82 | 83 | expect(chain).to.deep.equal(expectedChain); 84 | }); 85 | }); 86 | }); 87 | 88 | describe('buildPropertyAccessorChainFromAst', () => { 89 | describe('should return an accessor chain when', () => { 90 | it('AST has a single identifier', () => { 91 | const expectedChain = ['NOTIFY']; 92 | const script = ` 93 | callee(${expectedChain.join('.')}); 94 | `; 95 | const ast = espree.parse(script); 96 | const node = ast.body[0].expression.arguments[0]; 97 | const chain = utils.buildPropertyAccessorChainFromAst(node); 98 | 99 | expect(chain).to.deep.equal(expectedChain); 100 | }); 101 | 102 | it('AST has a nested "MemberExpression" node', () => { 103 | const expectedChain = ['EVENT', 'SIGNAL', 'NOTIFY']; 104 | const script = ` 105 | callee(${expectedChain.join('.')}); 106 | `; 107 | const ast = espree.parse(script); 108 | const node = ast.body[0].expression.arguments[0]; 109 | const chain = utils.buildPropertyAccessorChainFromAst(node); 110 | 111 | expect(chain).to.deep.equal(expectedChain); 112 | }); 113 | 114 | it('AST has a nested computed "MemberExpression" node', () => { 115 | const expectedChain = ['EVENT', 'SIGNAL', 'NOTIFY']; 116 | const script = ` 117 | callee(${expectedChain[0]}['${expectedChain[1]}'].${expectedChain[2]}); 118 | `; 119 | const ast = espree.parse(script); 120 | const node = ast.body[0].expression.arguments[0]; 121 | const chain = utils.buildPropertyAccessorChainFromAst(node); 122 | 123 | expect(chain).to.deep.equal(expectedChain); 124 | }); 125 | 126 | it('AST has only nested computed "MemberExpression" nodes', () => { 127 | const expectedChain = ['EVENT', 'SIGNAL', 'NOTIFY']; 128 | const script = ` 129 | callee(${expectedChain[0]}['${expectedChain[1]}']["${expectedChain[2]}"]); 130 | `; 131 | const ast = espree.parse(script); 132 | const node = ast.body[0].expression.arguments[0]; 133 | const chain = utils.buildPropertyAccessorChainFromAst(node); 134 | 135 | expect(chain).to.deep.equal(expectedChain); 136 | }); 137 | }); 138 | }); 139 | 140 | describe('buildObjectFromObjectExpression', () => { 141 | it('should build an object from an AST containing a nested "ObjectExpression" node', () => { 142 | const expectedObject = { 143 | SIGNAL: { 144 | NOTIFY: 'notify' 145 | } 146 | }; 147 | const script = ` 148 | var EVENT = { 149 | SIGNAL: { 150 | NOTIFY: 'notify' 151 | } 152 | }`; 153 | const ast = espree.parse(script); 154 | const node = ast.body[0].declarations[0].init; 155 | const object = utils.buildObjectFromObjectExpression(node); 156 | 157 | expect(object).to.deep.equal(expectedObject); 158 | }); 159 | 160 | it('should ignore unsupported node types', () => { 161 | const expectedObject = { 162 | SIGNAL: { 163 | NOTIFY: 'notify' 164 | }, 165 | LITERAL: true, 166 | }; 167 | const script = ` 168 | var EVENT = { 169 | SIGNAL: { 170 | NOTIFY: 'notify' 171 | }, 172 | OTHER: ['notify'], 173 | LITERAL: true, 174 | }`; 175 | const ast = espree.parse(script); 176 | const node = ast.body[0].declarations[0].init; 177 | const object = utils.buildObjectFromObjectExpression(node); 178 | 179 | expect(object).to.deep.equal(expectedObject); 180 | }); 181 | }); 182 | 183 | describe('getValueForPropertyAccessorChain', () => { 184 | it('should return the default value when value is unreachable', () => { 185 | const script = ` 186 | var EVENT = { 187 | SIGNAL: {} 188 | }`; 189 | const ast = espree.parse(script); 190 | const node = ast.body[0].declarations[0].init; 191 | const container = { 192 | EVENT: node 193 | }; 194 | const chain = ['EVENT', 'SIGNAL', 'NOTIFY']; 195 | const value = utils.getValueForPropertyAccessorChain(container, chain); 196 | 197 | expect(value).to.equal(utils.UNHANDLED_EVENT_NAME); 198 | }); 199 | 200 | it('should return the default value when visiting an unsupported node', () => { 201 | const script = ` 202 | var EVENT = { 203 | SIGNAL: ['notify'] 204 | }`; 205 | const ast = espree.parse(script); 206 | const node = ast.body[0].declarations[0].init; 207 | const container = { 208 | EVENT: node 209 | }; 210 | const chain = ['EVENT', 'SIGNAL', '0']; 211 | const value = utils.getValueForPropertyAccessorChain(container, chain); 212 | 213 | expect(value).to.equal(utils.UNHANDLED_EVENT_NAME); 214 | }); 215 | 216 | it('should return the correct value when searching an object', () => { 217 | const expectedValue = 'notify'; 218 | const script = ` 219 | var EVENT = { 220 | SIGNAL: { 221 | NOTIFY: 'notify' 222 | } 223 | }`; 224 | const ast = espree.parse(script); 225 | const node = ast.body[0].declarations[0].init; 226 | const container = { 227 | EVENT: node 228 | }; 229 | const chain = ['EVENT', 'SIGNAL', 'NOTIFY']; 230 | const value = utils.getValueForPropertyAccessorChain(container, chain); 231 | 232 | expect(value).to.equal(expectedValue); 233 | }); 234 | }); 235 | }); 236 | -------------------------------------------------------------------------------- /test/unit/options/options.spec.js: -------------------------------------------------------------------------------- 1 | const expect = require('chai').expect; 2 | 3 | const { 4 | validate, 5 | validateFeatures, 6 | retrieveFileOptions, 7 | OptionsError, 8 | ParserError 9 | } = require('../../../lib/options'); 10 | 11 | const baseOptions = { filename: 'empty.svelte' }; 12 | 13 | describe('Options Module', () => { 14 | describe('options.validate', () => { 15 | describe('Should throw when', () => { 16 | it('options object is missing', () => { 17 | expect(() => validate()).to.throw(OptionsError.OptionsRequired); 18 | }); 19 | 20 | it('input is missing, not a string, or an empty filename', () => { 21 | expect(() => validate({})).to.throw(OptionsError.InputRequired); 22 | expect(() => validate({ filename: {} })).to.throw(OptionsError.InputRequired); 23 | expect(() => validate({ filename: '' })).to.throw(OptionsError.InputRequired); 24 | expect(() => validate({ fileContent: {} })).to.throw(OptionsError.InputRequired); 25 | }); 26 | 27 | it('encoding is not a string', () => { 28 | const options = { ...baseOptions, encoding: true }; 29 | 30 | expect(() => validate(options)).to.throw(OptionsError.EncodingFormat); 31 | }); 32 | 33 | it('encoding is not supported', () => { 34 | const unsupported = 'l33t-enc'; 35 | const options = { ...baseOptions, encoding: unsupported }; 36 | 37 | expect(() => validate(options)).to.throw( 38 | OptionsError.EncodingNotSupported(unsupported) 39 | ); 40 | }); 41 | 42 | it('ignoreVisibilities is not an array', () => { 43 | const unsupported = 'unsupported'; 44 | const mixed = ['private', unsupported]; 45 | const options = { ...baseOptions, ignoredVisibilities: mixed }; 46 | 47 | expect(() => validate(options)).to.throw( 48 | OptionsError.IgnoredVisibilitiesNotSupported([unsupported]) 49 | ); 50 | }); 51 | 52 | it('ignoreVisibilities contains at least one unsupported visibility', () => { 53 | const unsupported = 'unsupported'; 54 | const mixed = ['private', unsupported]; 55 | const options = { ...baseOptions, ignoredVisibilities: mixed }; 56 | 57 | expect(() => validate(options)).to.throw( 58 | OptionsError.IgnoredVisibilitiesNotSupported([unsupported]) 59 | ); 60 | }); 61 | 62 | it('includeSourceLocations is not a boolean', () => { 63 | const options = { ...baseOptions, includeSourceLocations: 'true' }; 64 | 65 | expect(() => validate(options)).to.throw( 66 | OptionsError.IncludeSourceLocationsFormat 67 | ); 68 | }); 69 | }); 70 | 71 | describe('Should pass when', () => { 72 | it('just filename is present', () => { 73 | expect(() => validate(baseOptions)).to.not.throw(); 74 | }); 75 | 76 | it('just fileContent is present', () => { 77 | expect(() => validate({ fileContent: 'content' })).to.not.throw(); 78 | }); 79 | 80 | it('fileContent is empty', () => { 81 | expect(() => validate({ fileContent: '' })).to.not.throw(); 82 | }); 83 | 84 | it('ignoreVisibilities is an empty array', () => { 85 | const options = { ...baseOptions, ignoredVisibilities: [] }; 86 | 87 | expect(() => validate(options)).to.not.throw(); 88 | }); 89 | 90 | it('ignoreVisibilities is an array of supported visibilities', () => { 91 | const options = { 92 | ...baseOptions, 93 | ignoredVisibilities: ['protected', 'public'] 94 | }; 95 | 96 | expect(() => validate(options)).to.not.throw(); 97 | }); 98 | 99 | it('includeSourceLocations is a boolean', () => { 100 | const options1 = { ...baseOptions, includeSourceLocations: false }; 101 | const options2 = { ...baseOptions, includeSourceLocations: true }; 102 | 103 | expect(() => validate(options1)).to.not.throw(); 104 | expect(() => validate(options2)).to.not.throw(); 105 | }); 106 | }); 107 | }); 108 | 109 | describe('options.validateFeatures', () => { 110 | describe('Should pass when', () => { 111 | it('only supported features are present', () => { 112 | const supported = ['something', 'else']; 113 | 114 | const single = ['something']; 115 | const options1 = { features: single }; 116 | 117 | expect(() => validateFeatures(options1, supported)).to.not.throw(); 118 | 119 | const all = ['else', 'something']; 120 | const options2 = { features: all }; 121 | 122 | expect(() => validateFeatures(options2, supported)).to.not.throw(); 123 | }); 124 | }); 125 | 126 | describe('Should throw when', () => { 127 | it('features is not an array', () => { 128 | expect(() => validateFeatures({ features: {} }, [])) 129 | .to.throw(ParserError.FeaturesFormat); 130 | 131 | expect(() => validateFeatures({ features: true }, [])) 132 | .to.throw(ParserError.FeaturesFormat); 133 | 134 | expect(() => validateFeatures({ features: 'something' }, [])) 135 | .to.throw(ParserError.FeaturesFormat); 136 | }); 137 | 138 | it('features is an empty array', () => { 139 | const supported = ['something', 'else']; 140 | 141 | expect(() => validateFeatures({ features: [] }, supported)) 142 | .to.throw(ParserError.FeaturesEmpty(supported)); 143 | }); 144 | 145 | it('one or more features are not supported', () => { 146 | const supported = ['something', 'else']; 147 | 148 | const notSupported1 = ['other']; 149 | const options1 = { features: notSupported1 }; 150 | 151 | expect(() => validateFeatures(options1, supported)).to.throw( 152 | ParserError.FeaturesNotSupported(notSupported1, supported) 153 | ); 154 | 155 | const notSupported2 = ['other', 'bad', 'trash']; 156 | const options2 = { features: notSupported2 }; 157 | 158 | expect(() => validateFeatures(options2, supported)).to.throw( 159 | ParserError.FeaturesNotSupported(notSupported2, supported) 160 | ); 161 | }); 162 | 163 | it('some features are not supported', () => { 164 | const supported = ['something', 'else', 'stuff']; 165 | const notSupported = ['other', 'thing']; 166 | 167 | const mixed = ['stuff', ...notSupported, 'something']; 168 | const options2 = { features: mixed }; 169 | 170 | expect(() => validateFeatures(options2, supported)).to.throw( 171 | ParserError.FeaturesNotSupported(notSupported, supported) 172 | ); 173 | }); 174 | }); 175 | }); 176 | 177 | describe('options.retrieveFileOptions', () => { 178 | it('Should return all file-related keys from options', () => { 179 | expect(retrieveFileOptions(baseOptions)).to.have.keys( 180 | 'filename', 'fileContent', 'structure', 'encoding' 181 | ); 182 | }); 183 | }); 184 | }); 185 | --------------------------------------------------------------------------------