├── .babelrc ├── .editorconfig ├── .gitattributes ├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── DEVELOPMENT.md ├── GUIDELINES.md ├── LICENSE.md ├── Makefile ├── README.md ├── UPGRADE-2.0.md ├── assets └── header.svg ├── bin └── sassdoc ├── develop └── index.js ├── index.js ├── package.json ├── sache.json ├── src ├── annotation │ ├── annotations │ │ ├── access.js │ │ ├── alias.js │ │ ├── author.js │ │ ├── content.js │ │ ├── deprecated.js │ │ ├── example.js │ │ ├── group.js │ │ ├── groupDescription.js │ │ ├── ignore.js │ │ ├── index.js │ │ ├── link.js │ │ ├── name.js │ │ ├── output.js │ │ ├── parameter.js │ │ ├── property.js │ │ ├── require.js │ │ ├── return.js │ │ ├── see.js │ │ ├── since.js │ │ ├── throw.js │ │ ├── todo.js │ │ └── type.js │ └── index.js ├── cli.js ├── environment.js ├── errors.js ├── exclude.js ├── logger.js ├── notifier.js ├── parser.js ├── recurse.js ├── sassdoc.js ├── sorter.js └── utils.js ├── test ├── annotations │ ├── access.test.js │ ├── alias.test.js │ ├── api.test.js │ ├── author.test.js │ ├── content.test.js │ ├── defaults.test.js │ ├── deprecated.test.js │ ├── envMock.js │ ├── example.test.js │ ├── group.test.js │ ├── ignore.test.js │ ├── link.test.js │ ├── name.test.js │ ├── output.test.js │ ├── parameter.test.js │ ├── property.test.js │ ├── require.test.js │ ├── return.test.js │ ├── see.test.js │ ├── since.test.js │ ├── throw.test.js │ ├── todo.test.js │ └── type.test.js ├── api │ ├── documentize.test.js │ ├── exclude.test.js │ ├── parser.test.js │ ├── recurse.test.js │ └── stream.test.js ├── config-dest.yaml ├── config.yaml ├── data │ ├── expected.json │ ├── stream │ └── test.scss ├── env │ ├── environment.test.js │ └── logger.test.js ├── fixture │ ├── config.json │ ├── denodeify.txt │ ├── one.scss │ ├── three.scss │ └── two.scss ├── mock.js └── utils │ ├── errors.test.js │ └── utils.test.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["env", { 4 | "targets": { 5 | "browsers": ["last 2 versions", "> 1%"], 6 | "node": 4 7 | } 8 | }] 9 | ], 10 | "plugins": [ 11 | ["transform-runtime", { 12 | "polyfill": true, 13 | "regenerator": true 14 | }], 15 | ["babel-plugin-transform-builtin-extend", { 16 | "globals": ["Error"] 17 | }] 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | [*] 8 | indent_style = space 9 | indent_size = 2 10 | end_of_line = lf 11 | charset = utf-8 12 | trim_trailing_whitespace = true 13 | insert_final_newline = true 14 | 15 | [*.md] 16 | trim_trailing_whitespace = false 17 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text eol=lf 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /dist 3 | /test/data/expected.stream.json 4 | /test/custom-sassdoc 5 | /sassdoc 6 | /coverage 7 | .nyc_output 8 | .DS_Store 9 | npm-debug.log 10 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - 6 5 | - 8 6 | - lts/* 7 | - stable 8 | 9 | before_install: 10 | - npm i npm@latest -g 11 | 12 | sudo: false 13 | 14 | git: 15 | depth: 10 16 | 17 | script: make lint dist test cover 18 | after_success: make coveralls 19 | 20 | notifications: 21 | slack: 22 | secure: BiCHWsLTzlEd6MjRZY27BgmtCA3DmBFo5jcSsjSqmInbgla0lzDAA40xfIKTswIwK0pzuQdVaanl5Jy78wul0lfxpa5LLWX/bQmR1LGrygphC5tFG7GDM9Hdjatce5XEYVsSAUddNFEVQtMEUuUkSOiUll7kRq4LQtvAiIy1isk= 23 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 2.7.4 4 | 5 | * Support group in `@see` URL. [[SassDoc/sassdoc#567]](https://github.com/SassDoc/sassdoc/pull/567), 6 | [[SassDoc/sassdoc#465]](https://github.com/SassDoc/sassdoc/pull/465) and 7 | [[SassDoc/sassdoc-theme-default#116]](https://github.com/SassDoc/sassdoc-theme-default/pull/116). 8 | 9 | ## 2.7.3 10 | 11 | * Update dependencies to fix security vulnerabilities. 12 | Most notably `update-notifier`. 13 | 14 | ## 2.7.2 15 | 16 | * Update fs-vinyl dependency to fix security vulnerability. 17 | [(#547)](https://github.com/SassDoc/sassdoc/pull/547) 18 | 19 | ## 2.7.1 20 | 21 | * Allow SCSS parser stream errors to be caught. 22 | [(#540)](https://github.com/SassDoc/sassdoc/pull/540) 23 | 24 | ## 2.7.0 25 | 26 | * Update `sassdoc-theme-default`. 27 | Template engine replacement, swap Swig per Nunjucks. 28 | 29 | ## 2.6.0 30 | 31 | * Update `sassdoc-theme-default`. 32 | * Update `scss-comment-parser`. 33 | * Add group description parsing 34 | [(#534)](https://github.com/SassDoc/sassdoc/pull/534) 35 | 36 | ## 2.5.1 37 | 38 | * Update `sassdoc-theme-default`. 39 | Fixes an issue with main types pluralization. 40 | ([#101](https://github.com/SassDoc/sassdoc-theme-default/pull/101) 41 | 42 | ## 2.5.0 43 | 44 | * Added support for async annotation resolve functions. 45 | ([#517](https://github.com/SassDoc/sassdoc/issues/517), [#518](https://github.com/SassDoc/sassdoc/pull/518)) 46 | 47 | ## 2.4.0 48 | 49 | * Added support for scoped theme packages. 50 | ([#514](https://github.com/SassDoc/sassdoc/issues/514), [#515](https://github.com/SassDoc/sassdoc/pull/515)) 51 | 52 | ## 2.3.0 53 | 54 | * Themes can now pass an `includeUnknownContexts` config key up to the parser. 55 | Allows for including comments not necessarily linked to an item in final data. 56 | ([#498](https://github.com/SassDoc/sassdoc/pull/498)) 57 | 58 | ## 2.2.2 59 | 60 | * Fix the `verbose` and `strict` flags throwing an exception when being passed 61 | from config or API params. ([#491](https://github.com/SassDoc/sassdoc/issues/491), [#490](https://github.com/SassDoc/sassdoc/issues/490)) 62 | 63 | ## 2.2.1 64 | 65 | * Scope empty data message under the verbose flag. ([#488](https://github.com/SassDoc/sassdoc/issues/488), [#489](https://github.com/SassDoc/sassdoc/issues/489)) 66 | This prevent unwanted console cluttering on certain build setup. 67 | * Remove empty data warning about CSS selectors. 68 | This is no longer relevant, since CSS selectors are supported (although not being promoted yet). 69 | 70 | ## 2.2.0 71 | 72 | * Batch upgrade dependencies, fix security warnings. 73 | * Upgrade transpilation to Babel 6. 74 | 75 | ## 2.1.20 76 | 77 | * Add `argument` as an alias for `parameter` annotation. 78 | 79 | ## 2.1.19 80 | 81 | * Fix expected line numbers from scss-comment-parser. 82 | 83 | ## 2.1.18 84 | 85 | * Fix previous publish. 86 | 87 | ## 2.1.17 88 | 89 | * Downgrade scss-comment-parser. ([#438](https://github.com/SassDoc/sassdoc/issues/438), [#439](https://github.com/SassDoc/sassdoc/issues/439)) 90 | 91 | ## 2.1.16 92 | 93 | * Upgrade scss-comment-parser. ([#21](https://github.com/SassDoc/scss-comment-parser/pull/21)) 94 | 95 | ## 2.1.15 96 | 97 | * Update Babel to 5.5 because of a bug with Babel 5.1 runtime. 98 | 99 | ## 2.1.14 100 | 101 | * Make `access` work when not autofilled ([#399](https://github.com/SassDoc/sassdoc/issues/399)) 102 | 103 | ## 2.1.13 104 | 105 | * Default destination is relative to CWD ([#403](https://github.com/SassDoc/sassdoc/pull/403)) 106 | 107 | ## 2.1.12 108 | 109 | * More generic streaming success message ([#402](https://github.com/SassDoc/sassdoc/pull/402)) 110 | 111 | ## 2.1.11 112 | 113 | * Republish of 2.1.10 because of a failed npm publish. 114 | 115 | ## 2.1.10 116 | 117 | * Strip `@example` indent ([#401](https://github.com/SassDoc/sassdoc/pull/401)) 118 | 119 | ## 2.1.9 120 | 121 | * Upgrade to Babel 5.1, fix zn issue with `Symbol` ([#396](https://github.com/SassDoc/sassdoc/issues/396)) 122 | 123 | ## 2.1.8 124 | 125 | * Upgrade to Babel 5.0 ([#394](https://github.com/SassDoc/sassdoc/pull/394)) 126 | * Ensure `default` theme name is properly logged ([#393](https://github.com/SassDoc/sassdoc/pull/393)) 127 | * Several dependencies updates ([#392](https://github.com/SassDoc/sassdoc/pull/392)) 128 | * Switched to Eslint ([#388](https://github.com/SassDoc/sassdoc/pull/388)) 129 | 130 | ## 2.1.0 — Polite Little Squid 131 | 132 | * A [`@name` annotation](http://sassdoc.com/annotations/#name) has been added to make it possible to override an item's name ([#358](https://github.com/SassDoc/sassdoc/issues/358)) 133 | * A [`privatePrefix` option](http://sassdoc.com/customising-the-view/#private-prefix) has been added to make it possible to autofill `@access` ([#357](https://github.com/SassDoc/sassdoc/issues/357)) 134 | * [`description` and `descriptionPath` options](http://sassdoc.com/customising-the-view/#description) have been added to make it possible to provide a project wide description, as direct Markdown or a Markdown file ([#256](https://github.com/SassDoc/sassdoc/issues/256)) 135 | * A [`sort` option](http://sassdoc.com/customising-the-view/#sort) has been added to make it possible to order items based on a couple of criterias ([#239](https://github.com/SassDoc/sassdoc/issues/239)) 136 | 137 | ## 2.0.9 138 | 139 | * Update `sass-convert` version to prevent the 6to5-runtime issue described in ([#369](https://github.com/SassDoc/sassdoc/issues/369)) 140 | 141 | ## 2.0.8 142 | 143 | * Ensure specific 6to5 and 6to5-runtime version. ([#369](https://github.com/SassDoc/sassdoc/issues/369), 144 | [8bab189](https://github.com/SassDoc/sassdoc/commit/8bab18915b9fa7df6c764bb3211ecfb7acc491b1)) 145 | 146 | ## 2.0.7 147 | 148 | * Fix group sorting. ([e506be0](https://github.com/SassDoc/sassdoc/commit/e506be01df1bdbb378cdfa015b221b8ff72843d0)) 149 | 150 | ## 2.0.6 151 | 152 | * Graceful relative paths in CLI and config file. ([#362](https://github.com/SassDoc/sassdoc/issues/362), 153 | [c47dea1](https://github.com/SassDoc/sassdoc/commit/c47dea149771e995b1782fd917e48bec37df48f6)) 154 | 155 | ## 2.0.5 156 | 157 | * Fix an issue with relative path passed via CLI and configuration file ([#364](https://github.com/SassDoc/sassdoc/pull/364), [c47dea1](https://github.com/SassDoc/sassdoc/commit/c47dea149771e995b1782fd917e48bec37df48f6)) 158 | 159 | ## 2.0.4 160 | 161 | * Fix an issue with autofill and items that use a css keyword as name. ([#359](https://github.com/SassDoc/sassdoc/issues/359)) 162 | * Fix an issue where `.sassdocrc` was ignored when using Node.js API 163 | ([#363](https://github.com/SassDoc/sassdoc/issues/363)) 164 | 165 | ## 2.0.3 166 | 167 | * Fix the CLI synopsis, SassDoc can't be executed without arguments. 168 | 169 | ## 2.0.2 170 | 171 | * Move to 6to5 3 in `selfContained` mode to avoid global scope pollution ([#354](https://github.com/SassDoc/sassdoc/issues/354#issuecomment-72464640)) 172 | 173 | ## 2.0.1 174 | 175 | * Fix debug `task.name` value, returning proper function name instead of something like `callee$1$0`. 176 | 177 | ## 2.0.0 — Shiny Streamy Octopus 178 | 179 | ### API breaks for users 180 | 181 | * C-style comments (`/** */`) are no longer supported ([#326](https://github.com/SassDoc/sassdoc/issues/326)) 182 | * SassDoc now always outputs its own directory in the current folder ([#302](https://github.com/SassDoc/sassdoc/issues/302)) 183 | * `--dest` option has been added to define SassDoc's folder path for output, default is `sassdoc` ([#302](https://github.com/SassDoc/sassdoc/issues/302)) 184 | * `--no-prompt` option no longer exists since SassDoc outputs its own folder ([#302](https://github.com/SassDoc/sassdoc/issues/302)) 185 | * `--sass-convert` option no longer exists and is now implied ([#231](https://github.com/SassDoc/sassdoc/issues/231#issuecomment-63610647)) 186 | * Default name for configuration file is now `.sassdocrc` ([#189](https://github.com/SassDoc/sassdoc/issues/189)) 187 | * `@alias` can no longer be used on placeholders ([#263](https://github.com/SassDoc/sassdoc/issues/263)) 188 | * Annotations `@access`, `@content`, `@deprecated`, `@group`, `@output`, `@return` and `@type` are now restricted to one use per item ([#257](https://github.com/SassDoc/sassdoc/issues/257)) 189 | * Annotations `@param` and `@prop` now use square brackets (`[]`) for default values rather than parentheses (`()`) to avoid edge issues ([#303](https://github.com/SassDoc/sassdoc/issues/303)) 190 | * See [`UPGRADE-2.0.md`](https://github.com/SassDoc/sassdoc/blob/master/UPGRADE-2.0.md#annotations) to convert all your SassDoc comments. 191 | 192 | ### API breaks for theme builders 193 | 194 | * `sassdoc-filter` and `sassdoc-indexer` repositories no longer exist and have been replaced by [sassdoc-extras](https://github.com/SassDoc/sassdoc-extras) 195 | * `sassdoc-theme-light` repository no longer exists and has been replaced by [sassdoc-theme-default](https://github.com/SassDoc/sassdoc-theme-default) 196 | * `html*` properties no longer exist when using Markdown filter from sassdoc-extras, initial values are now overwritten ([sassdoc-extras#6](https://github.com/SassDoc/sassdoc-extras/pull/6)) 197 | * `@return` no longer returns a name and a default value, only a type and a description ([#277](https://github.com/SassDoc/sassdoc/issues/277)) 198 | * `@content` no longer returns an `autogenerated` key, only a description ([#262](https://github.com/SassDoc/sassdoc/issues/262)) 199 | * `parameters` key from item now becomes `parameter` ([#225](https://github.com/SassDoc/sassdoc/issues/225)) 200 | * `requires` key from item now becomes `require` ([#225](https://github.com/SassDoc/sassdoc/issues/225)) 201 | * `returns` key from item now becomes `return` ([#225](https://github.com/SassDoc/sassdoc/issues/225)) 202 | * `throws` key from item now becomes `throw` ([#225](https://github.com/SassDoc/sassdoc/issues/225)) 203 | * `todos` key from item now becomes `todo` ([#225](https://github.com/SassDoc/sassdoc/issues/225)) 204 | * `prop` key from item now becomes `property` ([#225](https://github.com/SassDoc/sassdoc/issues/225)) 205 | * When using the display filter from sassdoc-extras, items are now fully removed rather than given a `display` key ([sassdoc-extras#11](https://github.com/SassDoc/sassdoc-extras/issues/11)) 206 | 207 | ### API breaks for third party integration 208 | 209 | * The node API has been revamped and unified. 210 | * `sassdoc.documentize` does not exist anymore. 211 | * `sassdoc(src [, config])` execute the full Documentation process, returns a Promise. 212 | * `sassdoc([config])` execute the full Documentation process, returns a Stream of Vinyl files. 213 | * `sassdoc.parse(src [, config])` returns a Promise with the full data object. 214 | * `sassdoc.parse([config])` returns a Stream with the full data object. 215 | 216 | ### New features 217 | 218 | * The whole API has been fully refactored in ES6 ([#231](https://github.com/SassDoc/sassdoc/issues/231)) 219 | * `$` sign is now optional when defining the name in `@parameter` annotation ([#222](https://github.com/SassDoc/sassdoc/issues/222)) 220 | * It is now possible to use file/folder exclusion patterns ([#228](https://github.com/SassDoc/sassdoc/issues/231)) 221 | * It is now possible to pipe SassDoc into `stdin` ([#315](https://github.com/SassDoc/sassdoc/pull/315)) 222 | * `--debug` option has been added to output information about current setup ([#311](https://github.com/SassDoc/sassdoc/issues/311)) 223 | * Default theme now has a `googleAnalytics` configuration accepting a Google Analytics tracking key ([sassdoc-theme-default#10](https://github.com/SassDoc/sassdoc-theme-default/pull/10)) 224 | * Default theme now has a `trackingCode` configuration accepting an arbitrary HTML snippet to be included before `` ([sassdoc-theme-default#10](https://github.com/SassDoc/sassdoc-theme-default/pull/10)) 225 | * `@content` annotation is now correctly output in default theme ([sassdoc-theme-default#15](https://github.com/SassDoc/sassdoc-theme-default/issues/15)) 226 | * Default theme now displays the type as well as the name when cross-linking items (requirements, and so on...) ([sassdoc-theme-default#17](https://github.com/SassDoc/sassdoc-theme-default/issues/17)) 227 | * Error messages should now be more explicit, providing a file name and a line ([#282](https://github.com/SassDoc/sassdoc/issues/282)) 228 | * `--parse` option has been added to output raw parsing data as JSON from the CLI ([#318](https://github.com/SassDoc/sassdoc/issues/318)) 229 | 230 | ### Bug fixes 231 | 232 | * Autofill no longer capture `@throw` that are already defined with annotations ([#270](https://github.com/SassDoc/sassdoc/issues/270)) 233 | * Variables *view source* link now correctly supports start and end lines in default theme ([sassdoc-theme-default#21](https://github.com/SassDoc/sassdoc-theme-default/issues/21)) 234 | * Empty groups are no longer displayed in default theme ([sassdoc-theme-default#20](https://github.com/SassDoc/sassdoc-theme-default/issues/20)) 235 | * Variable content is no longer displayed as `safe` in ([sassdoc-theme-default#19](https://github.com/SassDoc/sassdoc-theme-default/issues/19)) 236 | * `@since` description is now parsed as Markdown in the default theme ([sassdoc-extras#8](https://github.com/SassDoc/sassdoc-extras/issues/8)) 237 | * `@deprecated` description is now parsed as Markdown in the default theme ([sassdoc-extras#7](https://github.com/SassDoc/sassdoc-extras/issues/7)) 238 | 239 | ## 1.10.12 240 | 241 | * Backport of `a994ed5` fix multiple require autofill ([#314](https://github.com/SassDoc/sassdoc/issues/314)) 242 | 243 | ## 1.10.11 244 | 245 | * Ensure `@todo` compat with docs and contrib ([#293](https://github.com/SassDoc/sassdoc/issues/293)) 246 | 247 | ## 1.10.6 248 | 249 | * Ensure proper type checking for `@see` annotation ([#291](https://github.com/SassDoc/sassdoc/issues/232)) 250 | 251 | ## 1.10.3 252 | 253 | * Prevented `@requires` to autofill dependency twice 254 | 255 | ## 1.10.2 256 | 257 | * Fixed an issue with the folder wiping safeguard always aborting if folder is not empty without even prompting 258 | 259 | ## 1.10.1 260 | 261 | * Updated a dependency in order to use new version of sassdoc-theme-default 262 | 263 | ## 1.10.0 264 | 265 | * Made annotations `@throws`, `@requires` and `@content` fill themselves so you don't have to, unless told otherwise through the [`autofill` option](http://sassdoc.com/configuration/#autofill) ([#232](https://github.com/SassDoc/sassdoc/issues/232), [#238](https://github.com/SassDoc/sassdoc/issues/238)) 266 | * Added the ability to define `--sass-convert`, `--no-update-identifier` and `--no-prompt` options within the configuration file instead of CLI only ([#247](https://github.com/SassDoc/sassdoc/issues/247)) 267 | * Merged [sassdoc-filter](https://github.com/sassdoc/sassdoc-filter) and [sassdoc-indexer](https://github.com/sassdoc/sassdoc-indexer) into [sassdoc-extras](https://github.com/sassdoc/sassdoc-extras); theme authors are asked to use the new repository 268 | 269 | ## 1.9.0 270 | 271 | * Added ability to use inline comments with `///` ([#143](https://github.com/SassDoc/sassdoc/issues/143)) 272 | * Added some safeguards when wiping the destination folder to avoid accidents ([#220](https://github.com/SassDoc/sassdoc/issues/220)) 273 | * Added `@content` annotation, which is auto-filled when `@content` Sass directive is being found in a mixin ([#226](https://github.com/SassDoc/sassdoc/issues/226)) 274 | * Added `@require` alias for `@requires` ([#221](https://github.com/SassDoc/sassdoc/issues/221)) 275 | * Added `@property` alias for `@prop` ([#221](https://github.com/SassDoc/sassdoc/issues/221)) 276 | * Made the `$` sign optional when writing the parameter name in `@param` ([#222](https://github.com/SassDoc/sassdoc/issues/222)) 277 | * Annotations that should not be associated to certain types (for instance `@param` for a variable) now emit a warning and are properly discarded by the parser ([CDocParser#4](https://github.com/FWeinb/CDocParser/issues/4)) 278 | 279 | ## 1.8.0 280 | 281 | * Added ability to add your own annotations to your theme ([#203](https://github.com/SassDoc/sassdoc/issues/203)) 282 | * Fixed an issue with items being randomly ordered ([#208](https://github.com/SassDoc/sassdoc/issues/208)) 283 | * Greatly improved sidebar from the theme 284 | 285 | ## 1.7.0 286 | 287 | * Added a `--sass-convert` option to perform Sass to SCSS conversion before running SassDoc 288 | ([#183](https://github.com/SassDoc/sassdoc/issues/183#issuecomment-56262743)) 289 | * Added the ability to define annotations at a file-level ([#190](https://github.com/SassDoc/sassdoc/issues/190)) 290 | * Improved SassDoc's behaviour when default theme is missing ([#207](https://github.com/SassDoc/sassdoc/pull/207)) 291 | * Slightly improved our logging message regarding the theme being used ([#206](https://github.com/SassDoc/sassdoc/issues/206)) 292 | * Moved some logic out of the theme's templates right into the index.js from the theme ([sassdoc-theme-light#40](https://github.com/SassDoc/sassdoc-theme-light/issues/40)) 293 | 294 | ## 1.6.1 295 | 296 | * Fixed a bug where some descriptions didn't allow line breaks ([#209](https://github.com/SassDoc/sassdoc/issues/209)) 297 | 298 | ## 1.6.0 299 | 300 | * Added a [Yeoman Generator](https://github.com/SassDoc/generator-sassdoc-theme) to make it easier to build themes ([#185](https://github.com/SassDoc/sassdoc/issues/185)) 301 | * Added YAML support for configuration files; default configuration file name is still `view`, either as `.json`, `.yaml` or `.yml` ([#184](https://github.com/SassDoc/sassdoc/issues/184)) 302 | * Added a message to warn against relying on the default configuration file name (`view.{json,yaml,yml}`) since it will break in version 2.0.0 in favor of `.sassdocrc` (which will support both format at once while being more semantic, less confusing and less likely to conflict with other projects) ([#194](https://github.com/SassDoc/sassdoc/issues/194)) 303 | * Fixed an issue when variable items' value contains a semi-colon ([#191](https://github.com/SassDoc/sassdoc/issues/191)) 304 | * Improved the light theme (better sidebar toggle with states stored in localStorage, better code toggle, better JavaScript structure, and better performance) 305 | * Added a `byType` key to sassdoc-indexer to help building themes 306 | 307 | ## 1.5.2 308 | 309 | * Added implicit type for required placeholders ([#197](https://github.com/SassDoc/sassdoc/issues/197)) 310 | 311 | ## 1.5.1 312 | 313 | * Used `stat` instead of `lstat` to support symlinks ([22a9b79](https://github.com/SassDoc/sassdoc/commit/22a9b7986e1eef2bf962bb9b1a48467d257ee398)) 314 | 315 | ## 1.5.0 316 | 317 | * Added `@prop` to allow deeper documentation for maps ([#25](https://github.com/SassDoc/sassdoc/issues/25)) 318 | * Fixed circular JSON dependencies when using raw data ([#181](https://github.com/SassDoc/sassdoc/issues/181)) 319 | * Added an option to provide a custom shortcut icon to the view ([#178](https://github.com/SassDoc/sassdoc/issues/178)) 320 | 321 | ## 1.4.1 322 | 323 | * Fixed a broken test 324 | 325 | ## 1.4.0 326 | 327 | * Updated favicon from theme to prevent 404 328 | * Changed a dependency 329 | * Added placeholder support ([#154](https://github.com/SassDoc/sassdoc/issues/154)) 330 | * Prevented a crash when using invalid annotations; throwing a warning instead 331 | * Added `@source` as an alias for `@link` to carry more semantic ([#170](https://github.com/SassDoc/sassdoc/issues/170)) 332 | 333 | ## 1.3.2 334 | 335 | * Fixed a broken test 336 | 337 | ## 1.3.1 338 | 339 | * Merged a branch that needed to be merged 340 | 341 | ## 1.3.0 342 | 343 | * Added `@output` as an equivalent for `@return` for mixins ([#133](https://github.com/SassDoc/sassdoc/issues/133)) 344 | * Added the ability to add a title to `@example` ([#145](https://github.com/SassDoc/sassdoc/issues/145)) 345 | * Added the ability to preview the code of an item when clicking on it ([#124](https://github.com/SassDoc/sassdoc/issues/124)) 346 | 347 | ## 1.2.0 348 | 349 | * Improved the way `@since` is parsed ([#128](https://github.com/SassDoc/sassdoc/issues/128)) 350 | * Moved theming to [Themeleon](https://github.com/themeleon/themeleon) ([#69](https://github.com/SassDoc/sassdoc/issues/69)) 351 | * Added a *view source* feature ([#117](https://github.com/SassDoc/sassdoc/issues/117)) 352 | * Added the `@group` annotation, as well as a way to alias groups in order to have friendly names ([#29](https://github.com/SassDoc/sassdoc/issues/29)) 353 | * Added moar tests ([#138](https://github.com/SassDoc/sassdoc/issues/138)) 354 | 355 | ## 1.1.6 356 | 357 | * Backport, fixed `found-at` with absolute path ([#156](https://github.com/SassDoc/sassdoc/pull/156)) 358 | 359 | ## 1.1.5 360 | 361 | * Fixed `@example` not being printed for variables ([#146](https://github.com/SassDoc/sassdoc/pull/146)) 362 | 363 | ## 1.1.4 364 | 365 | * Fixed some visual issues with `@requires` ([#132](https://github.com/SassDoc/sassdoc/pull/132)) 366 | 367 | ## 1.1.3 368 | 369 | * Removed a duplicated `deprecated` flag in the view 370 | 371 | ## 1.1.2 372 | 373 | * Fixed a bug with relative path to `package.json` file 374 | 375 | ## 1.1.1 376 | 377 | * Fixed a small issue with display path, sometimes adding an extra slash ([#68](https://github.com/SassDoc/sassdoc/issues/68)) 378 | 379 | ## 1.1.0 380 | 381 | * New design 382 | * Improved the `@requires` annotation to support external vendors, and custom URL ([#61](https://github.com/SassDoc/sassdoc/issues/61)) 383 | * Added a search engine to the generated documentation ([#46](https://github.com/SassDoc/sassdoc/issues/46)) 384 | * Fixed an issue with `@link` not working correctly ([#108](https://github.com/SassDoc/sassdoc/issues/108)) 385 | * Added `examples` to `.gitignore` 386 | 387 | ## 1.0.2 388 | 389 | * Fixed an issue with config path resolving to false ([#68](https://github.com/SassDoc/sassdoc/issues/68)) 390 | 391 | ## 1.0.1 392 | 393 | * Worked around a npm bug 394 | 395 | ## 1.0.0 396 | 397 | * Fixed an issue with a missing dependency 398 | * Prevented a weird bug with `@require` 399 | * Improved styles from the theme 400 | * Improved the way we deal with configuration resolving 401 | * Added an option to prevent the notifier check from happening 402 | * Merged `sassdoc-cli` back into the main repository 403 | * Fixed an issue with item count in console ([#102](https://github.com/SassDoc/sassdoc/issues/102)) 404 | * Made parameters table headers WAI 2.0 compliant ([#101](https://github.com/SassDoc/sassdoc/pull/101)) 405 | * Fixed a logic issue in the view 406 | * Fixed a syntax highlighting issue with functions and mixins ([#99](https://github.com/SassDoc/sassdoc/pull/99)) 407 | * Improved the way we deal with file path ([#98](https://github.com/SassDoc/sassdoc/pull/98)) 408 | * Made it possible to use `@`-starting lines within `@example` as long as they are indented ([#96](https://github.com/SassDoc/sassdoc/pull/96)) 409 | * Fixed a tiny parser issue ([#95](https://github.com/SassDoc/sassdoc/pull/95)) 410 | * Exposed the version number in `sassdoc.version` ([#91](https://github.com/SassDoc/sassdoc/pull/93)) 411 | * Implemented `update-notifier` ([#92](https://github.com/SassDoc/sassdoc/issues/92)) 412 | * Made it possible for SassDoc to create nested folders ([#89](https://github.com/SassDoc/sassdoc/issues/89)) 413 | * Renamed all repositories to follow strict guidelines ([#70](https://github.com/SassDoc/sassdoc/issues/70)) 414 | * Fixed an issue with empty documented items ([#84](https://github.com/SassDoc/sassdoc/issues/84)) 415 | * Normalized description in annotations ([#81](https://github.com/SassDoc/sassdoc/issues/81)) 416 | * Made requiring a variable less error-prone ([#74](https://github.com/SassDoc/sassdoc/issues/74)) 417 | * Fixed minor issues when parsing `@param` ([#59](https://github.com/SassDoc/sassdoc/issues/59), [#60](https://github.com/SassDoc/sassdoc/issues/60), [#62](https://github.com/SassDoc/sassdoc/issues/62)) 418 | * Fixed an issue with `@import` being parsed ([#73](https://github.com/SassDoc/sassdoc/issues/73)) 419 | * Added language detection to `@example` ([#54](https://github.com/SassDoc/sassdoc/issues/54)) 420 | * Major style changes ([#65](https://github.com/SassDoc/sassdoc/issues/65)) 421 | * Improved view/DOM/SCSS structure 422 | * Added Grunt ([#55](https://github.com/SassDoc/sassdoc/issues/55)) 423 | * Removed Makefile from core 424 | * Added Travis ([#63](https://github.com/SassDoc/sassdoc/issues/63)) 425 | * Minor code improvements in bin 426 | * Fixed an issue with bin 427 | * Fixed some little bugs in view 428 | * Changed `@datatype` to `@type` 429 | * Fixed a parsing bug with expanded licenses in package.json 430 | * Added a footer ([#57](https://github.com/SassDoc/sassdoc/issues/57)) 431 | * Changed the structure of `view.json` 432 | * Added license (MIT) ([#58](https://github.com/SassDoc/sassdoc/issues/58)) 433 | * Massively improved templates quality 434 | * Massively improved SCSS quality 435 | * Authorized markdown on `@author` 436 | * Added a favicon 437 | * Fixed tiny typo in console warning 438 | * Added anchor to each item ([#56](https://github.com/SassDoc/sassdoc/issues/56)) 439 | * Added back the `[Private]` annotation before private items' name 440 | * Added a `version` parameter to `view.json` that gets displayed right next to the title 441 | * Prevented empty sections in case items exist but are not displayed 442 | * Prevented broken links with requires and usedby in case of private items 443 | * Fixed an issue where links were not displayed 444 | * Added `--version` option ([#51](https://github.com/SassDoc/sassdoc/issues/51)) 445 | * Improved Sass and Swig structure 446 | * Improved the way we display `@deprecated` ([#50](https://github.com/SassDoc/sassdoc/issues/50)) 447 | * Added location where item was found 448 | * Moved view's stylesheets to Sass 449 | * Changed the folder structure 450 | * Moved `view.json` to `view/` folder 451 | * Prevented some broken links 452 | * Made the documentation responsive 453 | * Added PrismJS 454 | * Fixed an issue with `@requires` type 455 | * Fixed some formatting issues with `@example` 456 | * Fixed an issue prevented `@requires` form working if there was any `@alias` 457 | * Greatly improved the view 458 | * Fixed `@deprecated` not supporting a message 459 | * Added a trim to `@datatype` 460 | * Moved to a real parser ([CDocParser](https://github.com/FWeinb/CDocParser) and [ScssCommentParser](https://github.com/FWeinb/ScssCommentParser)) 461 | * Dropped support for inline comments (`//`) 462 | * Added the ability to document examples with `@example` 463 | * Variables are now documented exactly like functions and mixins, yet they have a `@datatype` directive to specify their type 464 | * Changed the structure of `view.json` 465 | 466 | ## 0.4.1 467 | 468 | * Improved the way we can impact the view with `view.json` 469 | 470 | ## 0.4.0 471 | 472 | * Added a way to impact the view with `view.json` 473 | 474 | ## 0.3.9 475 | 476 | * Greatly improved the way we deal with variables 477 | 478 | ## 0.3.8 479 | 480 | * Fixed documented items count in generated documentation 481 | * Improved the way things work when nothing gets documented 482 | 483 | ## 0.3.7 484 | 485 | * Allowed markdown syntax at more places 486 | 487 | ## 0.3.6 488 | 489 | * Authorized `spritemap` as a valid type 490 | 491 | ## 0.3.5 492 | 493 | * Changed the way we deal with assets dumping 494 | 495 | ## 0.3.4 496 | 497 | * Fixed an issue when dumping assets 498 | 499 | ## 0.3.3 500 | 501 | * Who knows? 502 | 503 | ## 0.3.2 504 | 505 | * Updated view 506 | 507 | ## 0.3.1 508 | 509 | * Fixed a potential path issue 510 | 511 | ## 0.3.0 512 | 513 | * Added `@since` 514 | 515 | ## 0.2.1 516 | 517 | * Updated the way we deal with `@param` and `@return` 518 | 519 | ## 0.1.0 520 | 521 | * Initial commit 522 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Code of Conduct 2 | 3 | As contributors and maintainers of this project, and in the interest of 4 | fostering an open and welcoming community, we pledge to respect all people who 5 | contribute through reporting issues, posting feature requests, updating 6 | documentation, submitting pull requests or patches, and other activities. 7 | 8 | We are committed to making participation in this project a harassment-free 9 | experience for everyone, regardless of level of experience, gender, gender 10 | identity and expression, sexual orientation, disability, personal appearance, 11 | body size, race, ethnicity, age, religion, or nationality. 12 | 13 | Examples of unacceptable behavior by participants include: 14 | 15 | * The use of sexualized language or imagery 16 | * Personal attacks 17 | * Trolling or insulting/derogatory comments 18 | * Public or private harassment 19 | * Publishing other's private information, such as physical or electronic 20 | addresses, without explicit permission 21 | * Other unethical or unprofessional conduct 22 | 23 | Project maintainers have the right and responsibility to remove, edit, or 24 | reject comments, commits, code, wiki edits, issues, and other contributions 25 | that are not aligned to this Code of Conduct, or to ban temporarily or 26 | permanently any contributor for other behaviors that they deem inappropriate, 27 | threatening, offensive, or harmful. 28 | 29 | By adopting this Code of Conduct, project maintainers commit themselves to 30 | fairly and consistently applying these principles to every aspect of managing 31 | this project. Project maintainers who do not follow or enforce the Code of 32 | Conduct may be permanently removed from the project team. 33 | 34 | This Code of Conduct applies both within project spaces and in public spaces 35 | when an individual is representing the project or its community. 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 38 | reported by contacting a project maintainer at kitty.giraudel@gmail.com. All 39 | complaints will be reviewed and investigated and will result in a response that 40 | is deemed necessary and appropriate to the circumstances. Maintainers are 41 | obligated to maintain confidentiality with regard to the reporter of an 42 | incident. 43 | 44 | 45 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 46 | version 1.3.0, available at 47 | [http://contributor-covenant.org/version/1/3/0/][version] 48 | 49 | [homepage]: http://contributor-covenant.org 50 | [version]: http://contributor-covenant.org/version/1/3/0/ 51 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | ## How to contribute? 4 | 5 | There are many things you can do to contribute SassDoc: 6 | 7 | * Report a bug 8 | * Fix a bug or implement a new feature 9 | * Suggest a new feature 10 | * Improve the documentation 11 | 12 | ## Asking a question 13 | 14 | Please, do not open an issue only to ask a question; it is not the best place for this. First, make sure your question is not already answered in the [documentation](http://sassdoc.com), especially in the [Frequently Asked Questions](http://sassdoc.com/frequently-asked-questions/) section. 15 | 16 | If it's not, you can leave your question on: 17 | 18 | * [SassDoc on Slack](http://sassdoc.slack.com/) 19 | * [#sassdoc on freenode](http://webchat.freenode.net/) 20 | 21 | If you need a quick reply or don't feel really comfortable with asking on a public channel, try getting in touch with us on Twitter: [@SassDoc_](https://twitter.com/sassdoc_), the official account. 22 | 23 | ## Filling a bug 24 | 25 | So you think you've found a bug? Likely. We're all humans after all! Before even opening an issue, you have to know how SassDoc is architected so you can submit an issue in the accurate repository: 26 | 27 | * [Core](https://github.com/sassdoc/sassdoc): API (you're here) 28 | * [Theme](https://github.com/sassdoc/sassdoc-theme-default): Theme, templates and styles 29 | * [Extras](https://github.com/sassdoc/sassdoc-extras): Extra tools for theme authors 30 | * [Grunt plugin](https://github.com/sassdoc/grunt-sassdoc): Grunt integration 31 | * [Gulp plugin](https://github.com/sassdoc/gulp-sassdoc): Gulp integration 32 | * [Broccoli plugin](https://github.com/sassdoc/broccoli-sassdoc): Broccoli integration 33 | * [Yeoman Theme generator](https://github.com/sassdoc/generator-theme-sassdoc): Theme generator 34 | * [Site](https://github.com/sassdoc/sassdoc.github.io): SassDoc's site 35 | * [Syntax converter](https://github.com/sassdoc/sass-convert): Sass to SCSS converter and the like 36 | * [Blank theme](https://github.com/sassdoc/sassdoc-theme-blank): Blank theme 37 | 38 | Please give GitHub's search a try first, to make sure someone didn't already submit something similar. If what you've found seems unique, be sure to open an issue in the appropriate repository with a clear title and a description as complete as possible. 39 | 40 | Avoid titles like "it doesn't work", "please help me", or "bug with sassdoc". It doesn't help much understanding the problem at first glance. 41 | 42 | Similarly, be as descriptive as possible within your description. If you have a stack trace, post it. If you can make a screenshot, even better! Explain the problem and how you got there so we can reproduce it. Only then we'll be able to investigate. 43 | 44 | Posting informations about your current environment and setup is always helpful. 45 | Hopefully, SassDoc can gather everything for you, just run it with the `--debug` flag when using CLI, or set `SASSDOC_DEBUG=1` environment variable (e.g. `SASSDOC_DEBUG=1 grunt` when using Grunt). 46 | 47 | ## Submitting a pull request 48 | 49 | Want to contribute? Awesome! 50 | 51 | If it's a little something like typo, styles or wording, feel free to jump on it and submit a pull request; chances are high it will be quickly merged. 52 | 53 | If you are thinking of something a bit more complex, be sure to talk with someone from the team before even starting coding in order to avoid any useless work. Also be sure to read our [code guidelines](GUIDELINES.md). 54 | 55 | New features or sizeable refactors should be based on the `develop` branch while bugfixes should head into both `master` and `develop`. 56 | 57 | Anyway, thank you very much for contributing! 58 | -------------------------------------------------------------------------------- /DEVELOPMENT.md: -------------------------------------------------------------------------------- 1 | # How to develop on SassDoc 2 | 3 | ## How to have a fresh install of SassDoc 4 | 5 | ```sh 6 | # Clone the repository on your machine 7 | git clone git@github.com:SassDoc/sassdoc.git 8 | 9 | # If you don't have a SSH key, feel free to clone using HTTPS instead 10 | # git clone https://github.com/SassDoc/sassdoc.git 11 | 12 | # If you're not a SassDoc organisation member and you forked the repo, 13 | # you need to clone `yourname/sassdoc.git` instead. 14 | # 15 | # Also add actual SassDoc repo as `upstream` to fetch code updates: 16 | git remote add upstream https://github.com/SassDoc/sassdoc.git 17 | 18 | # Head into the local repository 19 | cd sassdoc 20 | 21 | # Move to `develop` branch 22 | git checkout develop 23 | 24 | # Install node modules 25 | npm install 26 | 27 | # Run all Make tasks 28 | make 29 | ``` 30 | 31 | ## How to develop a feature 32 | 33 | ```sh 34 | # Be sure to be on an up-to-date `develop` 35 | git checkout develop 36 | git pull 37 | 38 | # If you're on a fork, be sure to pull the `upstream` version 39 | git pull upstream develop 40 | 41 | # Create a new feature branch 42 | git checkout -b feature/my-new-feature 43 | 44 | # Add some work 45 | 46 | # Make a beautiful commit, reference related issues if needed 47 | git commit 48 | 49 | # Push your branch 50 | git push -u origin feature/my-new-feature 51 | 52 | # Then make a pull request (targeting `develop`)! 53 | ``` 54 | 55 | ## How to make an hotfix 56 | 57 | ```sh 58 | # Be sure to be on an up-to-date `master` 59 | git checkout master 60 | git pull 61 | 62 | # If you're on a fork, be sure to pull the `upstream` version 63 | git pull upstream master 64 | 65 | # Create a new hotfix branch 66 | git checkout -b hotfix/my-new-hotfix 67 | 68 | # Add the actual fix 69 | 70 | # Commit the fix, reference related issues 71 | git commit 72 | 73 | # Push your branch 74 | git push -u origin hotfix/my-new-hotfix 75 | 76 | # Make a pull request if it's relevant 77 | ``` 78 | 79 | ## How to merge an hotfix 80 | 81 | ```sh 82 | # Merge in `master` 83 | git checkout master 84 | git pull 85 | git merge --no-ff hotfix/hotfix-to-merge 86 | 87 | # Tag the hotfix version (patch should be bumped in branch) 88 | git tag 89 | 90 | # Push 91 | git push 92 | git push --tags 93 | 94 | # Merge in `develop` and push 95 | git checkout develop 96 | git pull 97 | git merge --no-ff hotfix/hotfix-to-merge 98 | git push 99 | 100 | # Delete hotfix branch 101 | git branch -d hotfix/hotfix-to-merge 102 | git push origin :hotfix/hotfix-to-merge 103 | ``` 104 | 105 | ## How to release a new version 106 | 107 | ```sh 108 | # Move to `develop` branch and get latest changes 109 | git checkout develop 110 | git pull 111 | 112 | # Bump version number in `package.json` 113 | vim package.json 114 | 115 | # Commit the change in `package.json` 116 | git add package.json 117 | git commit -m 'Bump ' 118 | 119 | # Push on `develop` 120 | git push 121 | 122 | # Head to `master` 123 | git checkout master 124 | git pull 125 | 126 | # Merge `develop` branch 127 | git merge --no-ff develop 128 | 129 | # Tag the commit 130 | git tag 131 | 132 | # Run tests one last time and publish the package 133 | make publish 134 | 135 | # Push 136 | git push 137 | git push --tags 138 | ``` 139 | 140 | Then on GitHub, [add a new release](https://github.com/SassDoc/sassdoc/releases/new) with both *Tag version* and *Release title* matching the new version. The *description* should be the changelog. 141 | 142 | ## How to release a pre-version 143 | 144 | Same as [How to release a new version](#how-to-release-a-new-version) except it's not merged in `master` and the `npm publish` is slightly different: 145 | 146 | ```sh 147 | # Move to `develop` branch and get latest changes 148 | git checkout develop 149 | git pull 150 | 151 | # Bump version number in `package.json` 152 | vim package.json 153 | 154 | # Commit the change in `package.json` 155 | git add package.json 156 | git commit -m 'Bump ' 157 | 158 | # Push on `develop` 159 | git push 160 | 161 | # Run tests one last time 162 | make test 163 | 164 | # Publish the package 165 | npm publish --tag beta 166 | ``` 167 | 168 | ## How to work on the theme 169 | 170 | ```sh 171 | # Make sure you have the latest version of SassDoc 172 | npm update sassdoc -g 173 | 174 | # Clone the repository on your machine 175 | git clone git@github.com:SassDoc/sassdoc-theme-default.git 176 | 177 | # If you don't have a SSH key, feel free to clone using HTTPS instead 178 | # git clone https://github.com/SassDoc/sassdoc-theme-default.git 179 | 180 | # Head into the local repository 181 | cd sassdoc-theme-default 182 | 183 | # Run all Make tasks 184 | make 185 | 186 | # Run SassDoc 187 | sassdoc scss/ --theme ./ --verbose 188 | ``` 189 | 190 | When you make a change: 191 | 192 | ```sh 193 | # Run all Make tasks and SassDoc 194 | make all && sassdoc scss/ --theme ./ --verbose 195 | ``` 196 | -------------------------------------------------------------------------------- /GUIDELINES.md: -------------------------------------------------------------------------------- 1 | # JavaScript coding guidelines 2 | 3 | This is an extension to [Node Styleguide](https://github.com/felixge/node-style-guide) by Felix Geisendörfer. Anything from this document overrides what could be said in the Node Styleguide. 4 | 5 | **Always lint code before pushing.** 6 | 7 | ## EOF 8 | 9 | Always add a trailing blank line at end of file. 10 | 11 | ## String interpolation 12 | 13 | When a string contains a variable, use backticks rather than single quotes to wrap it. 14 | 15 | ```js 16 | console.log(`Theme: #{value}.`); 17 | ``` 18 | 19 | ## Module imports 20 | 21 | ```js 22 | import { foo } from './foo'; 23 | ``` 24 | 25 | ## Module exports 26 | 27 | Exported functions and classes should always have a name, even if it's not required per see. Naming exports help debugging and figuring out what's going on. 28 | 29 | ```js 30 | export default function foo() {} 31 | export default class Bar {} 32 | ``` 33 | 34 | ## Object description 35 | 36 | Objects with 1 or 2 key/value pairs can be written either on a single line or on multiple lines, with no extra comma. 37 | 38 | ```js 39 | let obja = { foo: 'foo' }; 40 | let objb = { foo: 'foo', bar: 'bar' }; 41 | 42 | f({ foo: 'foo' }); 43 | f({ foo: 'foo', bar: 'bar' }); 44 | ``` 45 | 46 | Objects with at least 3 key/value pairs should be written with each pair on its own line. Last pair also has a trailing comma to make it easier to move lines around. 47 | 48 | ```js 49 | let obj = { 50 | foo: 'foo', 51 | bar: 'bar', 52 | baz: 'baz', 53 | }; 54 | 55 | f({ 56 | foo: 'foo', 57 | bar: 'bar', 58 | baz: 'baz', 59 | }); 60 | ``` 61 | 62 | ## Variable declaration 63 | 64 | Variable declarations, no matter with `var` or `let` should not be aligned. 65 | 66 | ```js 67 | let a = 1; 68 | let ba = 2; 69 | ``` 70 | 71 | ## Arrow functions 72 | 73 | ```js 74 | let x = () => { 75 | // ... 76 | }; 77 | ``` 78 | 79 | ## Default arguments in functions 80 | 81 | Equal symbol (`=`) should always be surrounded by spaces when defining default values for arguments in a function signature. 82 | 83 | ```js 84 | function foo(bar = 'baz') {} 85 | ``` 86 | 87 | ## Code blocks 88 | 89 | Whenever writing an `if`/`else` statement, the `else` keyword should directly follow the closing curly brace from the `if` statement. No line break between the closing brace and the keyword should exist. 90 | 91 | Also, there should always be a single space between the `if` keyword and the condition. 92 | 93 | ```js 94 | if (condition) { 95 | // ... 96 | } else if (condition) { 97 | // ... 98 | } else { 99 | // ... 100 | } 101 | ``` 102 | 103 | Same goes with do/while and try/catch. 104 | 105 | ## Inline documentation 106 | 107 | Every function should be documented using [JSDoc](http://usejsdoc.org/). Annotations should not be aligned in order not to have to update alignment whever a longer line is added. 108 | 109 | ```js 110 | /** 111 | * @param {String} foo 112 | * @return {Boolean} 113 | * @see something 114 | */ 115 | ``` 116 | 117 | ## Comments 118 | 119 | All comments, both inline and multiline should be written in proper English, starting with a cap, ending with a full stop. Inline comments (`//`) are used when the comment holds on a single line (<80 characters). Multiline C-style comments (`/** */`) are used when the comment is splitted across several lines. 120 | 121 | ```js 122 | // Post process configuration. 123 | cfg.post(config); 124 | 125 | config.view = config; // Backward compatibility. 126 | 127 | /** 128 | * This is an extra long comment that does not fit on a single line because it 129 | * is longer than 80 characters. Because of this, it it splitted across several 130 | * lines. 131 | */ 132 | ``` 133 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 SassDoc 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | BIN = $(PWD)/node_modules/.bin/ 2 | SASSDOC = $(PWD)/bin/sassdoc 3 | 4 | all: lint dist test 5 | 6 | # Compile ES6 from `src` to ES5 in `dist` 7 | # ======================================= 8 | 9 | dist: 10 | rm -rf $@ 11 | $(BIN)babel src -d $@ 12 | 13 | # Code quality 14 | # ============ 15 | 16 | lint: 17 | $(BIN)standard bin/sassdoc index.js src/**/*.js test/**/*.js 18 | 19 | test: test/data/expected.stream.json dist 20 | $(BIN)mocha test/**/*.test.js 21 | $(SASSDOC) --parse test/data/test.scss | diff - test/data/expected.json 22 | $(SASSDOC) --parse - < test/data/test.scss | diff - test/data/expected.stream.json 23 | rm -rf sassdoc && $(SASSDOC) test/data/test.scss && [ -d sassdoc ] 24 | rm -rf sassdoc && $(SASSDOC) - < test/data/test.scss && [ -d sassdoc ] 25 | rm -rf sassdoc && $(SASSDOC) -c test/config.yaml test/data/test.scss && [ -d sassdoc ] 26 | rm -rf sassdoc test/custom-sassdoc && $(SASSDOC) -c test/config-dest.yaml test/data/test.scss && [ -d test/custom-sassdoc ] 27 | 28 | test/data/expected.stream.json: test/data/expected.json 29 | test/data/stream $< > $@ 30 | 31 | cover: 32 | rm -rf coverage 33 | $(BIN)nyc $(BIN)mocha test/**/*.test.js 34 | 35 | cover-browse: dist 36 | rm -rf coverage 37 | $(BIN)nyc --reporter=html $(BIN)mocha test/**/*.test.js 38 | $(BIN)opn coverage/index.html 39 | 40 | coveralls: 41 | ($(BIN)nyc report --reporter=text-lcov | $(BIN)coveralls) || exit 0 42 | 43 | 44 | # Development 45 | # =========== 46 | 47 | develop: 48 | $(BIN)babel-node $@ 49 | 50 | # Publish package to npm 51 | # @see npm/npm#3059 52 | # ======================= 53 | 54 | publish: all 55 | npm publish 56 | 57 | # Release, publish 58 | # ================ 59 | 60 | # "patch", "minor", "major", "prepatch", 61 | # "preminor", "premajor", "prerelease" 62 | VERS ?= "patch" 63 | TAG ?= "latest" 64 | 65 | release: all 66 | npm version $(VERS) -m "Release %s" 67 | npm publish --tag $(TAG) 68 | git push --follow-tags 69 | 70 | # Tools 71 | # ===== 72 | 73 | rebuild: 74 | rm -rf node_modules 75 | npm install 76 | 77 | .PHONY: dist test develop 78 | .SILENT: dist develop cover view-cover travis 79 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![SassDoc](https://cdn.rawgit.com/SassDoc/sassdoc/master/assets/header.svg)](http://sassdoc.com) 2 | 3 | [![Build Status][travis-image]][travis-url] 4 | [![Coverage Status][coveralls-image]][coveralls-url] 5 | [![Dependencies Status][depstat-image]][depstat-url] 6 | [![License][license-image]][license-url] 7 | [![Gitter][chat-image]][chat-url] 8 | 9 | [![NPM][npm-image]][npm-url] 10 | 11 | ## Documentation 12 | 13 | Head to the [site][SassDoc] for extensive documentation. 14 | 15 | ## Credits 16 | 17 | * [Valérian Galliat](https://twitter.com/valeriangalliat) 18 | * [Fabrice Weinberg](https://twitter.com/fweinb) 19 | * [Pascal Duez](https://twitter.com/pascalduez) 20 | * [Kitty Giraudel](http://twitter.com/KittyGiraudel) 21 | 22 | [SassDoc]: http://sassdoc.com 23 | [npm-url]: https://www.npmjs.com/package/sassdoc 24 | [npm-image]: https://nodei.co/npm/sassdoc.png?downloads=true 25 | [travis-url]: https://travis-ci.org/SassDoc/sassdoc?branch=master 26 | [travis-image]: http://img.shields.io/travis/SassDoc/sassdoc.svg?style=flat-square 27 | [coveralls-url]: https://coveralls.io/r/SassDoc/sassdoc?branch=master 28 | [coveralls-image]: https://img.shields.io/coveralls/SassDoc/sassdoc.svg?style=flat-square 29 | [depstat-url]: https://david-dm.org/SassDoc/sassdoc 30 | [depstat-image]: https://david-dm.org/SassDoc/sassdoc.svg?style=flat-square 31 | [license-image]: http://img.shields.io/npm/l/sassdoc.svg?style=flat-square 32 | [license-url]: LICENSE.md 33 | [chat-image]: https://img.shields.io/badge/gitter-join%20chat-blue.svg?style=flat-square 34 | [chat-url]: https://gitter.im/SassDoc/sassdoc 35 | -------------------------------------------------------------------------------- /UPGRADE-2.0.md: -------------------------------------------------------------------------------- 1 | # Upgrade from 1.0 to 2.0 2 | 3 | ## C-style comments 4 | 5 | The support of C-style `/**` comments [has been dropped][issue-326]. 6 | You can use the following scripts to upgrade your codebase to `///` 7 | comments if you were using them: 8 | 9 | **GNU `sed`:** 10 | 11 | ```sh 12 | find . -name '*.s[ac]ss' -exec sed -i 's,^/\*\*,///,;s,^ *\*\*/,////,;s,^ *\*/,///,;s,^ *\*,///,' {} + 13 | ``` 14 | 15 | **Mac/BSD `sed`:** 16 | 17 | ```sh 18 | find . -name '*.s[ac]ss' -exec sed -i '' 's,^/\*\*,///,;s,^ *\*\*/,////,;s,^ *\*/,///,;s,^ *\*,///,' {} + 19 | ``` 20 | 21 | The script can't handle all the possible edge cases, so please make sure 22 | to run it in a version-controlled environment, and review carefully the 23 | changes. 24 | 25 | You'll also have to manually fix all your [poster comments] by hand 26 | since there is no way for this script to convert them accurately; only 27 | the closing can be converted, but you'll need to add a `/` to all poster 28 | openings. 29 | 30 | If you want to know more on what's happening in these cryptic `sed` 31 | commands, here is the commented `sed` source: 32 | 33 | ```sh 34 | # Opening (can't determine if poster or normal) 35 | s,^/\*\*,///, 36 | 37 | # Poster closing 38 | s,^ *\*\*/,////, 39 | 40 | # Normal closing 41 | s,^ *\*/,///, 42 | 43 | # Comment body 44 | s,^ *\*,///, 45 | ``` 46 | 47 | [issue-326]: https://github.com/SassDoc/sassdoc/issues/326 48 | [poster comments]: http://sassdoc.com/file-level-annotations/ 49 | 50 | ## Annotations 51 | 52 | The default value of some annotations is now inside square brackets 53 | instead of parentheses. This affects `@param` and `@prop`. 54 | 55 | **Before:** 56 | 57 | ```scss 58 | /// @param {String} $foo (bar) - Baz. 59 | @function baz($foo) {} 60 | ``` 61 | 62 | **After:** 63 | 64 | ```scss 65 | /// @param {String} $foo [bar] - Baz. 66 | @function baz($foo) {} 67 | ``` 68 | 69 | You can use the following script to update your codebase, though 70 | it will replace all the parentheses by square brackets in the lines 71 | containing the affected annotations, which may not be what you want 72 | (for example if there's parentheses inside the default value or in 73 | the description). 74 | 75 | Be sure to review the changes made by the script and eventually fix 76 | details by hand. 77 | 78 | 79 | **GNU `sed`:** 80 | 81 | ```sh 82 | find . -type f -name '*.s[ac]ss' -exec sed -ri '/@param|@prop/y/()/[]/' {} + 83 | ``` 84 | 85 | **BSD/Mac `sed`:** 86 | 87 | ```sh 88 | find . -type f -name '*.s[ac]ss' -exec sed -Ei '' '/@param|@prop/y/\(\)/\[\]/' {} + 89 | ``` 90 | 91 | ## CLI 92 | 93 | The CLI usage slightly changed, since the destination is now optional 94 | and configurable with an option instead of an argument, so you'll have 95 | to update your scripts using SassDoc if you have any. 96 | 97 | **Before:** 98 | 99 | ```sh 100 | sassdoc scss/ doc/ 101 | ``` 102 | 103 | **After:** 104 | 105 | ```sh 106 | sassdoc scss/ --dest doc/ 107 | ``` 108 | 109 | When you don't give a destination, SassDoc will put the documentation in 110 | a `sassdoc` folder in the current directory. 111 | 112 | **SassDoc will wipe the whole destination folder upon each run, so be 113 | sure you don't have anything important in it.** 114 | 115 | ## Node 116 | 117 | The `documentize` function from 1.0 is now the default export from the 118 | `sassdoc` module. 119 | 120 | **Before:** 121 | 122 | ```js 123 | var sassdoc = require('sassdoc'); 124 | 125 | sassdoc.documentize('scss/').then(function () { 126 | console.log('All done!'); 127 | }); 128 | ``` 129 | 130 | **After:** 131 | 132 | ```js 133 | var sassdoc = require('sassdoc'); 134 | 135 | sassdoc('scss/').then(function () { 136 | console.log('All done!'); 137 | }); 138 | ``` 139 | 140 | The `parse` function still works the same way as in 1.0. 141 | 142 | ## Gulp 143 | 144 | Starting from version 2.0 SassDoc core is fully Gulp compatible, and can be directly 145 | integrated in any Vinyl files pipeline. The [Gulp plugin](https://github.com/SassDoc/gulp-sassdoc) is now deprecated. 146 | Refer to the [documentation](http://sassdoc.com/gulp/) for full examples. 147 | 148 | Similarly to any Gulp plugin, passing a directory path to `gulp.src` won't work anymore. 149 | You have to pass in glob patterns: `'source/**/*.scss'` 150 | -------------------------------------------------------------------------------- /assets/header.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /bin/sassdoc: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | 'use strict' 4 | 5 | require('../dist/cli').default(process.argv.slice(2)) 6 | -------------------------------------------------------------------------------- /develop/index.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import chalk from 'chalk'; 3 | import dateformat from 'dateformat'; 4 | import vfs from 'vinyl-fs'; 5 | import through from 'through2'; 6 | import sassdoc from '../src/sassdoc'; 7 | 8 | function devLog(...args) { 9 | console.log(...[ 10 | chalk.styles.inverse.open, 11 | `[${dateformat(new Date(), 'HH:MM:ss')}]`, 12 | ...args, 13 | chalk.styles.inverse.close 14 | ]); 15 | } 16 | 17 | function inspect() { 18 | let count = 0; 19 | 20 | return through.obj((chunk, enc, cb) => { 21 | count++; 22 | cb(null, chunk); 23 | }, (cb) => { 24 | devLog(`develop:stream:count:${count}`); 25 | cb(); 26 | }); 27 | } 28 | 29 | function documentize() { 30 | return sassdoc('./test/data', { verbose: true }) 31 | .then(() => { 32 | devLog('develop:documentize:end'); 33 | }); 34 | } 35 | 36 | function stream() { 37 | let parse = sassdoc({ verbose: true }); 38 | 39 | vfs.src('./test/data/**/*.scss') 40 | .pipe(parse) 41 | .on('end', () => { 42 | devLog('develop:stream:end'); 43 | }) 44 | .pipe(inspect()) 45 | .on('data', () => {}); 46 | 47 | return parse.promise.then(() => { 48 | devLog('develop:stream:promise:end'); 49 | }); 50 | } 51 | 52 | (async function () { 53 | try { 54 | await documentize(); 55 | await stream(); 56 | } 57 | catch (err) { 58 | console.error(err); 59 | } 60 | }()); 61 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var sassdoc = require('./dist/sassdoc') 4 | 5 | module.exports = sassdoc.default 6 | module.exports.parseFilter = sassdoc.parseFilter 7 | module.exports.ensureEnvironment = sassdoc.ensureEnvironment 8 | module.exports.parse = sassdoc.parse 9 | module.exports.Environment = sassdoc.Environment 10 | module.exports.Logger = sassdoc.Logger 11 | module.exports.Parser = sassdoc.Parser 12 | module.exports.sorter = sassdoc.sorter 13 | module.exports.errors = sassdoc.errors 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sassdoc", 3 | "description": "Release the docs!", 4 | "author": { 5 | "name": "Kitty Giraudel", 6 | "url": "http://kittygiraudel.com", 7 | "email": "kitty.giraudel@gmail.com" 8 | }, 9 | "contributors": [ 10 | { 11 | "name": "Fabrice Weinberg", 12 | "url": "https://twitter.com/fweinb", 13 | "email": "fabrice@weinberg.me" 14 | }, 15 | { 16 | "name": "Valérian Galliat", 17 | "url": "https://val.codejam.info" 18 | }, 19 | { 20 | "name": "Pascal Duez", 21 | "url": "https://twitter.com/pascalduez" 22 | } 23 | ], 24 | "version": "2.7.4", 25 | "license": "MIT", 26 | "repository": { 27 | "type": "git", 28 | "url": "https://github.com/SassDoc/sassdoc" 29 | }, 30 | "homepage": "http://sassdoc.com", 31 | "keywords": [ 32 | "sass", 33 | "scss", 34 | "css", 35 | "doc", 36 | "documentation", 37 | "comments", 38 | "theme" 39 | ], 40 | "bugs": { 41 | "url": "https://github.com/SassDoc/sassdoc/issues" 42 | }, 43 | "bin": { 44 | "sassdoc": "bin/sassdoc" 45 | }, 46 | "browser": { 47 | "./dist/notifier.js": false, 48 | "./dist/exclude.js": false, 49 | "./dist/cli.js": false, 50 | "./dist/recurse.js": false, 51 | "vinyl-fs": false, 52 | "glob2base": false, 53 | "docopt": false, 54 | "glob": false, 55 | "js-yaml": false, 56 | "minimatch": false, 57 | "mkdirp": false, 58 | "multipipe": false, 59 | "rimraf": false, 60 | "safe-wipe": false, 61 | "sass-convert": false, 62 | "sassdoc-theme-default": false, 63 | "update-notifier": false, 64 | "through2": false, 65 | "concat-stream": false, 66 | "vinyl-source-stream": false 67 | }, 68 | "files": [ 69 | "bin", 70 | "dist", 71 | "index.js", 72 | "CHANGELOG.md", 73 | "UPGRADE-2.0.md", 74 | "LICENCE.md", 75 | "README.md" 76 | ], 77 | "scripts": { 78 | "test": "make" 79 | }, 80 | "standard": { 81 | "env": { 82 | "mocha": true 83 | } 84 | }, 85 | "nyc": { 86 | "exclude": [ 87 | "**/*.test.js", 88 | "test" 89 | ] 90 | }, 91 | "dependencies": { 92 | "ansi-styles": "^4.2.1", 93 | "babel-runtime": "^6.26.0", 94 | "chalk": "^2.4.2", 95 | "concat-stream": "^2.0.0", 96 | "docopt": "^0.6.1", 97 | "glob": "^7.1.6", 98 | "glob2base": "0.0.12", 99 | "js-yaml": "^3.14.0", 100 | "lodash.difference": "^4.5.0", 101 | "lodash.uniq": "^4.5.0", 102 | "minimatch": "^3.0.4", 103 | "mkdirp": "^1.0.4", 104 | "multipipe": "1.0.2", 105 | "rimraf": "^3.0.2", 106 | "safe-wipe": "0.2.5", 107 | "sass-convert": "^0.5.0", 108 | "sassdoc-theme-default": "^2.8.3", 109 | "scss-comment-parser": "^0.8.4", 110 | "strip-indent": "^3.0.0", 111 | "through2": "1.1.1", 112 | "update-notifier": "^4.1.0", 113 | "vinyl-fs": "^3.0.3", 114 | "vinyl-source-stream": "1.1.2", 115 | "vinyl-string": "^1.0.2" 116 | }, 117 | "devDependencies": { 118 | "babel-cli": "^6.26.0", 119 | "babel-plugin-transform-builtin-extend": "^1.1.2", 120 | "babel-plugin-transform-runtime": "^6.22.0", 121 | "babel-preset-env": "^1.7.0", 122 | "coveralls": "^3.1.0", 123 | "dateformat": "^3.0.3", 124 | "jsesc": "^2.5.2", 125 | "mocha": "^3.4.2", 126 | "nyc": "^11.2.1", 127 | "opn-cli": "^3.1.0", 128 | "safe-buffer": "^5.2.1", 129 | "sinon": "^2.3.4", 130 | "standard": "^10.0.3", 131 | "vinyl": "^2.2.0" 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /sache.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "SassDoc", 3 | "description": "Release the docs!", 4 | "tags": ["sass", "documentation", "functions", "mixins", "variables", "placeholders", "theme"] 5 | } 6 | -------------------------------------------------------------------------------- /src/annotation/annotations/access.js: -------------------------------------------------------------------------------- 1 | export default function access (env) { 2 | 3 | const defaultPrivatePrefixTest = RegExp.prototype.test.bind(/^[_-]/) 4 | 5 | return { 6 | name: 'access', 7 | 8 | parse (text) { 9 | return text.trim() 10 | }, 11 | 12 | autofill (item) { 13 | if (item.access !== 'auto') { 14 | return 15 | } 16 | 17 | if (env.privatePrefix === false) { 18 | return 19 | } 20 | 21 | let testFunc = defaultPrivatePrefixTest 22 | 23 | if (typeof env.privatePrefix !== 'undefined') { 24 | testFunc = RegExp.prototype.test.bind(new RegExp(env.privatePrefix)) 25 | } 26 | 27 | if (testFunc(item.context.name)) { 28 | return 'private' 29 | } 30 | 31 | return 'public' 32 | }, 33 | 34 | resolve (data) { 35 | data.forEach(item => { 36 | // Ensure valid access when not autofilled. 37 | if (item.access === 'auto') { 38 | item.access = 'public' 39 | } 40 | }) 41 | }, 42 | 43 | default () { 44 | return 'auto' 45 | }, 46 | 47 | multiple: false, 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/annotation/annotations/alias.js: -------------------------------------------------------------------------------- 1 | export default function alias (env) { 2 | return { 3 | name: 'alias', 4 | 5 | parse (text) { 6 | return text.trim() 7 | }, 8 | 9 | resolve (data) { 10 | data.forEach(item => { 11 | if (item.alias === undefined) { 12 | return 13 | } 14 | 15 | let alias = item.alias 16 | let name = item.context.name 17 | let aliasGroup = item.group 18 | 19 | let aliasedItem = Array.find(data, i => i.context.name === alias) 20 | 21 | if (aliasedItem === undefined) { 22 | env.logger.warn(`Item \`${name}\` is an alias of \`${alias}\` but this item doesn't exist.`) 23 | delete item.alias 24 | return 25 | } 26 | 27 | if (!Array.isArray(aliasedItem.aliased)) { 28 | aliasedItem.aliased = [] 29 | } 30 | 31 | if (!Array.isArray(aliasedItem.aliasedGroup)) { 32 | aliasedItem.aliasedGroup = []; 33 | } 34 | 35 | aliasedItem.aliased.push(name) 36 | aliasedItem.aliasedGroup.push({ group: aliasGroup, name: name }) 37 | }) 38 | }, 39 | 40 | allowedOn: ['function', 'mixin', 'variable'], 41 | 42 | multiple: false, 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/annotation/annotations/author.js: -------------------------------------------------------------------------------- 1 | export default function author () { 2 | return { 3 | name: 'author', 4 | 5 | parse (text) { 6 | return text.trim() 7 | }, 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/annotation/annotations/content.js: -------------------------------------------------------------------------------- 1 | export default function content () { 2 | return { 3 | name: 'content', 4 | 5 | parse (text) { 6 | return text.trim() 7 | }, 8 | 9 | autofill (item) { 10 | if (!item.content && item.context.code.indexOf('@content') > -1) { 11 | return '' 12 | } 13 | }, 14 | 15 | allowedOn: ['mixin'], 16 | 17 | multiple: false, 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/annotation/annotations/deprecated.js: -------------------------------------------------------------------------------- 1 | export default function deprecated () { 2 | 3 | return { 4 | name: 'deprecated', 5 | 6 | parse (text) { 7 | return text.trim() 8 | }, 9 | 10 | multiple: false, 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/annotation/annotations/example.js: -------------------------------------------------------------------------------- 1 | /** 2 | * `@example` is a multiline annotation. 3 | * 4 | * Check if there is something on the first line and use it as the type information. 5 | * 6 | * @example html - description 7 | *
8 | */ 9 | 10 | const stripIndent = require('strip-indent') 11 | const descRegEx = /(\w+)\s*(?:-?\s*(.*))/ 12 | 13 | export default function example () { 14 | return { 15 | name: 'example', 16 | 17 | parse (text) { 18 | let instance = { 19 | type: 'scss', // Default to `scss`. 20 | code: text, 21 | } 22 | 23 | // Get the optional type info. 24 | let optionalType = text.substr(0, text.indexOf('\n')) 25 | 26 | if (optionalType.trim().length !== 0) { 27 | let typeDesc = descRegEx.exec(optionalType) 28 | instance.type = typeDesc[1] 29 | if (typeDesc[2].length !== 0) { 30 | instance.description = typeDesc[2] 31 | } 32 | instance.code = text.substr(optionalType.length + 1) // Remove the type 33 | } 34 | 35 | // Remove all leading/trailing line breaks. 36 | instance.code = instance.code.replace(/^\n|\n$/g, '') 37 | 38 | instance.code = stripIndent(instance.code) 39 | 40 | return instance 41 | }, 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/annotation/annotations/group.js: -------------------------------------------------------------------------------- 1 | export default function group () { 2 | return { 3 | name: 'group', 4 | 5 | parse (text, info) { 6 | let lines = text.trim().split('\n') 7 | let slug = lines[0].trim().toLowerCase() 8 | let description = lines.splice(1).join('\n').trim() 9 | if (description) { 10 | info.groupDescriptions = info.groupDescriptions || {} 11 | info.groupDescriptions[slug] = description 12 | } 13 | return [slug] 14 | }, 15 | 16 | default () { 17 | return ['undefined'] 18 | }, 19 | 20 | multiple: false, 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/annotation/annotations/groupDescription.js: -------------------------------------------------------------------------------- 1 | /** 2 | * `@groupDescriptions` is should not be used on its own. 3 | * 4 | * It gets filled automatically from the lines following the `@group` annotation. 5 | * 6 | * @group example 7 | * This is a group description. It describes the group. 8 | * It can be split across multiple lines. 9 | * 10 | * { 11 | * 'groupDescriptions': { 12 | * 'example': 'This is a group description. It describes the group.\nIt can be split across multiple lines.' 13 | * } 14 | * } 15 | */ 16 | 17 | export default function groupDescriptions () { 18 | return { 19 | name: 'groupDescriptions', 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/annotation/annotations/ignore.js: -------------------------------------------------------------------------------- 1 | export default function ignore() { 2 | return { 3 | name: 'ignore', 4 | 5 | parse () {}, 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/annotation/annotations/index.js: -------------------------------------------------------------------------------- 1 | module.exports = [ 2 | require('./access.js').default, 3 | require('./alias.js').default, 4 | require('./author.js').default, 5 | require('./content.js').default, 6 | require('./deprecated.js').default, 7 | require('./example.js').default, 8 | require('./group.js').default, 9 | require('./groupDescription.js').default, 10 | require('./ignore.js').default, 11 | require('./link.js').default, 12 | require('./name.js').default, 13 | require('./output.js').default, 14 | require('./parameter.js').default, 15 | require('./property.js').default, 16 | require('./require.js').default, 17 | require('./return.js').default, 18 | require('./see.js').default, 19 | require('./since.js').default, 20 | require('./throw.js').default, 21 | require('./todo.js').default, 22 | require('./type.js').default 23 | ] 24 | -------------------------------------------------------------------------------- /src/annotation/annotations/link.js: -------------------------------------------------------------------------------- 1 | const linkRegex = /\s*([^:]+\:\/\/[^\s]*)?\s*(.*?)$/ 2 | 3 | export default function link () { 4 | return { 5 | name: 'link', 6 | 7 | parse (text) { 8 | let parsed = linkRegex.exec(text.trim()) 9 | 10 | return { 11 | url: parsed[1] || '', 12 | caption: parsed[2] || '', 13 | } 14 | }, 15 | 16 | alias: ['source'], 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/annotation/annotations/name.js: -------------------------------------------------------------------------------- 1 | export default function name () { 2 | return { 3 | name: 'name', 4 | 5 | parse (text) { 6 | return text.trim() 7 | }, 8 | 9 | // Abuse the autofill feature to rewrite the `item.context` 10 | autofill (item) { 11 | if (item.name) { 12 | item.context.name = item.name 13 | // Cleanup 14 | delete item.name 15 | } 16 | }, 17 | 18 | multiple: false 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/annotation/annotations/output.js: -------------------------------------------------------------------------------- 1 | export default function output () { 2 | return { 3 | name: 'output', 4 | 5 | parse (text) { 6 | return text.trim() 7 | }, 8 | 9 | alias: ['outputs'], 10 | 11 | allowedOn: ['mixin'], 12 | 13 | multiple: false, 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/annotation/annotations/parameter.js: -------------------------------------------------------------------------------- 1 | const typeRegEx = /^\s*(?:\{(.*)\})?\s*(?:\$?([^\s^\]\[]+))?\s*(?:\[([^\]]*)\])?\s*(?:-?\s*([\s\S]*))?/ 2 | 3 | export default function parameter (env) { 4 | return { 5 | name: 'parameter', 6 | 7 | parse (text, info, id) { 8 | let parsed = typeRegEx.exec(text) 9 | let obj = {} 10 | 11 | if (parsed[1]) { 12 | obj.type = parsed[1] 13 | } 14 | 15 | if (parsed[2]) { 16 | obj.name = parsed[2] 17 | } else { 18 | env.logger.warn(`@parameter must at least have a name. Location: ${id}:${info.commentRange.start}:${info.commentRange.end}`) 19 | return undefined 20 | } 21 | 22 | if (parsed[3]) { 23 | obj.default = parsed[3] 24 | } 25 | 26 | if (parsed[4]) { 27 | obj.description = parsed[4] 28 | } 29 | 30 | return obj 31 | }, 32 | 33 | alias: ['arg', 'argument', 'arguments', 'param', 'parameters'], 34 | 35 | allowedOn: ['function', 'mixin'], 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/annotation/annotations/property.js: -------------------------------------------------------------------------------- 1 | const reqRegEx = /\s*(?:{(.*)})?\s*(?:(\$?\S+))?\s*(?:\[([^\]]*)])?\s*-?\s*([\S\s]*)\s*$/ 2 | 3 | export default function property () { 4 | return { 5 | name: 'property', 6 | 7 | parse (text) { 8 | let match = reqRegEx.exec(text.trim()) 9 | 10 | let obj = { 11 | type: match[1] || 'Map', 12 | } 13 | 14 | if (match[2]) { 15 | obj.name = match[2] 16 | } 17 | 18 | if (match[3]) { 19 | obj.default = match[3] 20 | } 21 | 22 | if (match[4]) { 23 | obj.description = match[4] 24 | } 25 | 26 | return obj 27 | }, 28 | 29 | alias: ['prop'], 30 | 31 | allowedOn: ['variable'], 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/annotation/annotations/require.js: -------------------------------------------------------------------------------- 1 | import { splitNamespace } from '../../utils' 2 | import uniq from 'lodash.uniq' 3 | 4 | const reqRegEx = /^\s*(?:\{(.*)\})?\s*(?:(\$?[^\s]+))?\s*(?:-?\s*([^<$]*))?\s*(?:)?$/ 5 | 6 | export default function require_ (env) { 7 | return { 8 | name: 'require', 9 | 10 | parse (text) { 11 | let match = reqRegEx.exec(text.trim()) 12 | 13 | let obj = { 14 | type: match[1] || 'function', 15 | name: match[2], 16 | } 17 | 18 | obj.external = splitNamespace(obj.name).length > 1 19 | 20 | if (obj.name.indexOf('$') === 0) { 21 | obj.type = 'variable' 22 | obj.name = obj.name.slice(1) 23 | } 24 | 25 | if (obj.name.indexOf('%') === 0) { 26 | obj.type = 'placeholder' 27 | obj.name = obj.name.slice(1) 28 | } 29 | 30 | if (match[3]) { 31 | obj.description = match[3].trim() 32 | } 33 | 34 | if (match[4]) { 35 | obj.url = match[4] 36 | } 37 | 38 | return obj 39 | }, 40 | 41 | autofill (item) { 42 | let type = item.context.type 43 | 44 | if (type === 'mixin' || type === 'placeholder' || type === 'function') { 45 | let handWritten 46 | 47 | if (item.require) { 48 | handWritten = {} 49 | 50 | item.require.forEach(reqObj => { 51 | handWritten[reqObj.type + '-' + reqObj.name] = true 52 | }) 53 | } 54 | 55 | let mixins = searchForMatches( 56 | item.context.code, 57 | /@include\s+([^(;$]*)/ig, 58 | isAnnotatedByHand.bind(null, handWritten, 'mixin') 59 | ) 60 | 61 | let functions = searchForMatches( 62 | item.context.code, 63 | new RegExp('(@include)?\\s*([a-z0-9_-]+)\\s*\\(', 'ig'), // Literal destorys Syntax 64 | isAnnotatedByHand.bind(null, handWritten, 'function'), 65 | 2 // Get the second matching group instead of 1 66 | ) 67 | 68 | let placeholders = searchForMatches( 69 | item.context.code, 70 | /@extend\s*%([^;\s]+)/ig, 71 | isAnnotatedByHand.bind(null, handWritten, 'placeholder') 72 | ) 73 | 74 | let variables = searchForMatches( 75 | item.context.code, 76 | /\$([a-z0-9_-]+)/ig, 77 | isAnnotatedByHand.bind(null, handWritten, 'variable') 78 | ) 79 | 80 | // Create object for each required item. 81 | mixins = mixins.map(typeNameObject('mixin')) 82 | functions = functions.map(typeNameObject('function')) 83 | placeholders = placeholders.map(typeNameObject('placeholder')) 84 | variables = variables.map(typeNameObject('variable')) 85 | 86 | // Merge all arrays. 87 | let all = [].concat(mixins, functions, placeholders, variables) 88 | 89 | // Merge in user supplyed requires if there are any. 90 | if (item.require && item.require.length > 0) { 91 | all = all.concat(item.require) 92 | } 93 | 94 | // Filter empty values. 95 | all = all.filter(x => { 96 | return x !== undefined 97 | }) 98 | 99 | if (all.length > 0) { 100 | all = uniq(all, 'name') 101 | 102 | // Filter the item itself. 103 | all = all.filter(x => { 104 | return !(x.name === item.context.name && x.type === item.context.type) 105 | }) 106 | 107 | return all 108 | } 109 | } 110 | }, 111 | 112 | resolve (data) { 113 | data.forEach(item => { 114 | if (item.require === undefined) { 115 | return 116 | } 117 | 118 | item.require = item.require.map(req => { 119 | if (req.external === true) { 120 | return req 121 | } 122 | 123 | let reqItem = Array.find(data, x => x.context.name === req.name && x.context.type === req.type) 124 | 125 | if (reqItem === undefined) { 126 | if (!req.autofill) { 127 | env.logger.warn( 128 | `Item \`${item.context.name}\` requires \`${req.name}\` from type \`${req.type}\` but this item doesn't exist.` 129 | ) 130 | } 131 | 132 | return 133 | } 134 | 135 | if (!Array.isArray(reqItem.usedBy)) { 136 | reqItem.usedBy = [] 137 | 138 | reqItem.usedBy.toJSON = function () { 139 | return reqItem.usedBy.map(item => { 140 | return { 141 | description: item.description, 142 | context: item.context, 143 | } 144 | }) 145 | } 146 | } 147 | 148 | reqItem.usedBy.push(item) 149 | req.item = reqItem 150 | 151 | return req 152 | }) 153 | .filter(x => x !== undefined) 154 | 155 | if (item.require.length > 0) { 156 | item.require.toJSON = function () { 157 | return item.require.map(item => { 158 | let obj = { 159 | type: item.type, 160 | name: item.name, 161 | external: item.external, 162 | } 163 | 164 | if (item.external) { 165 | obj.url = item.url 166 | } else { 167 | obj.description = item.description 168 | obj.context = item.context 169 | } 170 | 171 | return obj 172 | }) 173 | } 174 | } 175 | }) 176 | }, 177 | 178 | alias: ['requires'], 179 | } 180 | } 181 | 182 | function isAnnotatedByHand (handWritten, type, name) { 183 | if (type && name && handWritten) { 184 | return handWritten[type + '-' + name] 185 | } 186 | 187 | return false 188 | } 189 | 190 | function searchForMatches (code, regex, isAnnotatedByHandProxy, id = 1) { 191 | let match 192 | let matches = [] 193 | 194 | while ((match = regex.exec(code))) { 195 | if (!isAnnotatedByHandProxy(match[id]) && (id <= 1 || match[id-1] === undefined)) { 196 | matches.push(match[id]) 197 | } 198 | } 199 | 200 | return matches 201 | } 202 | 203 | function typeNameObject (type) { 204 | return function (name) { 205 | if (name.length > 0) { 206 | return { 207 | type: type, 208 | name: name, 209 | autofill: true, 210 | } 211 | } 212 | } 213 | } 214 | -------------------------------------------------------------------------------- /src/annotation/annotations/return.js: -------------------------------------------------------------------------------- 1 | const typeRegEx = /^\s*(?:\{(.*)\})?\s*(?:-?\s*([\s\S]*))?/ 2 | 3 | export default function return_ (env) { 4 | return { 5 | name: 'return', 6 | 7 | parse (text, info, id) { 8 | let parsed = typeRegEx.exec(text) 9 | let obj = {} 10 | 11 | if (parsed[1]) { 12 | obj.type = parsed[1] 13 | } else { 14 | env.logger.warn(`@return must at least have a type. Location: ${id}:${info.commentRange.start}:${info.commentRange.end}`) 15 | return undefined 16 | } 17 | 18 | if (parsed[2]) { 19 | obj.description = parsed[2] 20 | } 21 | 22 | return obj 23 | }, 24 | 25 | alias: ['returns'], 26 | 27 | allowedOn: ['function'], 28 | 29 | multiple: false, 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/annotation/annotations/see.js: -------------------------------------------------------------------------------- 1 | const seeRegEx = /\s*(?:\{([\w-_]+)\}\s*)?(.*)/ 2 | 3 | export default function see (env) { 4 | return { 5 | name: 'see', 6 | 7 | parse (text) { 8 | let match = seeRegEx.exec(text) 9 | 10 | let obj = { 11 | type: match[1] || 'function', 12 | name: match[2] 13 | } 14 | 15 | if (obj.name.indexOf('$') === 0) { 16 | obj.type = 'variable' 17 | obj.name = obj.name.slice(1) 18 | } 19 | 20 | if (obj.name.indexOf('%') === 0) { 21 | obj.type = 'placeholder' 22 | obj.name = obj.name.slice(1) 23 | } 24 | 25 | return obj 26 | }, 27 | 28 | resolve (data) { 29 | data.forEach(item => { 30 | if (item.see === undefined) { 31 | return 32 | } 33 | 34 | item.see = item.see.map(see => { 35 | let seeItem = Array.find(data, x => x.context.name === see.name) 36 | 37 | if (seeItem !== undefined) { 38 | return seeItem 39 | } 40 | 41 | env.logger.warn(`Item \`${item.context.name}\` refers to \`${see.name}\` from type \`${see.type}\` but this item doesn't exist.`) 42 | }) 43 | .filter(x => x !== undefined) 44 | 45 | item.see.toJSON = function () { 46 | return item.see.map(item => { 47 | return { 48 | description: item.description, 49 | context: item.context, 50 | group: item.group, 51 | } 52 | }) 53 | } 54 | }) 55 | }, 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/annotation/annotations/since.js: -------------------------------------------------------------------------------- 1 | const sinceRegEx = /\s*([^\s]*)\s*(?:-?\s*([\s\S]*))?\s*$/ 2 | 3 | export default function since () { 4 | return { 5 | name: 'since', 6 | 7 | parse (text) { 8 | let parsed = sinceRegEx.exec(text) 9 | let obj = {} 10 | 11 | if (parsed[1]) { 12 | obj.version = parsed[1] 13 | } 14 | 15 | if (parsed[2]) { 16 | obj.description = parsed[2] 17 | } 18 | 19 | return obj 20 | }, 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/annotation/annotations/throw.js: -------------------------------------------------------------------------------- 1 | import uniq from 'lodash.uniq' 2 | 3 | const autoParserError = /@error\s+(?:'|")([^'"]+)/g 4 | 5 | export default function throw_ () { 6 | return { 7 | name: 'throw', 8 | 9 | parse (text) { 10 | return text.trim() 11 | }, 12 | 13 | autofill (item) { 14 | let match 15 | let throwing = item.throws || [] 16 | 17 | while ((match = autoParserError.exec(item.context.code))) { 18 | throwing.push(match[1]) 19 | } 20 | 21 | if (throwing.length > 0) { 22 | return uniq(throwing) 23 | } 24 | }, 25 | 26 | alias: ['throws', 'exception'], 27 | 28 | allowedOn: ['function', 'mixin', 'placeholder'], 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/annotation/annotations/todo.js: -------------------------------------------------------------------------------- 1 | export default function todo () { 2 | return { 3 | name: 'todo', 4 | 5 | parse (text) { 6 | return text.trim() 7 | }, 8 | 9 | alias: ['todos'], 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/annotation/annotations/type.js: -------------------------------------------------------------------------------- 1 | export default function type () { 2 | return { 3 | name: 'type', 4 | 5 | parse (text) { 6 | return text.trim() 7 | }, 8 | 9 | allowedOn: ['variable'], 10 | 11 | multiple: false, 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/annotation/index.js: -------------------------------------------------------------------------------- 1 | import annotations from './annotations' 2 | 3 | export default class AnnotationsApi { 4 | constructor (env) { 5 | this.env = env 6 | 7 | this.list = { 8 | _: { alias: {} } 9 | } 10 | 11 | this.addAnnotations(annotations) 12 | } 13 | 14 | /** 15 | * Add a single annotation by name 16 | * @param {String} name - Name of the annotation 17 | * @param {Object} annotation - Annotation object 18 | */ 19 | addAnnotation (name, annotation) { 20 | annotation = annotation(this.env) 21 | 22 | this.list._.alias[name] = name 23 | 24 | if (Array.isArray(annotation.alias)) { 25 | annotation.alias.forEach(aliasName => { 26 | this.list._.alias[aliasName] = name 27 | }) 28 | } 29 | 30 | this.list[name] = annotation 31 | } 32 | 33 | /** 34 | * Add an array of annotations. The name of the annotations must be 35 | * in the `name` key of the annotation. 36 | * @param {Array} annotations - Annotation objects 37 | */ 38 | addAnnotations (annotations) { 39 | if (annotations !== undefined) { 40 | annotations.forEach(annotation => { 41 | this.addAnnotation(annotation().name, annotation) 42 | }) 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/cli.js: -------------------------------------------------------------------------------- 1 | let doc = ` 2 | Usage: 3 | sassdoc - [options] 4 | sassdoc ... [options] 5 | 6 | Arguments: 7 | Path to your Sass folder. 8 | 9 | Options: 10 | -h, --help Bring help. 11 | --version Show version. 12 | -v, --verbose Enable verbose mode. 13 | -d, --dest= Documentation folder. 14 | -c, --config= Path to JSON/YAML configuration file. 15 | -t, --theme= Theme to use. 16 | -p, --parse Parse the input and output JSON data to stdout. 17 | --no-update-notifier Disable update notifier check. 18 | --strict Turn warnings into errors. 19 | --debug Output debugging information. 20 | ` 21 | 22 | import Environment from './environment' 23 | import Logger from './logger' 24 | import sassdoc, { parse } from './sassdoc' 25 | import * as errors from './errors' 26 | import { docopt } from 'docopt' 27 | import source from 'vinyl-source-stream' 28 | import pkg from '../package.json' 29 | 30 | export default function cli (argv = process.argv.slice(2)) { 31 | let options = docopt(doc, { version: pkg.version, argv: argv }) 32 | 33 | if (!options['-'] && !options[''].length) { 34 | // Trigger help display. 35 | docopt(doc, { version: pkg.version, argv: ['--help'] }) 36 | } 37 | 38 | let logger = new Logger(options['--verbose'], options['--debug'] || process.env.SASSDOC_DEBUG) 39 | let env = new Environment(logger, options['--verbose'], options['--strict']) 40 | 41 | logger.debug('argv:', () => JSON.stringify(argv)) 42 | 43 | env.on('error', (error) => { 44 | if (error instanceof errors.Warning) { 45 | process.exit(2) 46 | } 47 | 48 | process.exit(1) 49 | }) 50 | 51 | env.load(options['--config']) 52 | 53 | // Ensure CLI options. 54 | ensure(env, options, { 55 | dest: '--dest', 56 | theme: '--theme', 57 | noUpdateNotifier: '--no-update-notifier' 58 | }) 59 | 60 | env.postProcess() 61 | 62 | // Run update notifier if not explicitely disabled. 63 | if (!env.noUpdateNotifier) { 64 | require('./notifier').default(pkg, logger) 65 | } 66 | 67 | let handler, cb 68 | 69 | // Whether to parse only or to documentize. 70 | if (!options['--parse']) { 71 | handler = sassdoc 72 | cb = () => {} 73 | } else { 74 | handler = parse 75 | cb = data => console.log(JSON.stringify(data, null, 2)) 76 | } 77 | 78 | if (options['-']) { 79 | return process.stdin 80 | .pipe(source()) 81 | .pipe(handler(env)) 82 | .on('data', cb) 83 | } 84 | 85 | handler(options[''], env).then(cb) 86 | } 87 | 88 | /** 89 | * Ensure that CLI options take precedence over configuration values. 90 | * 91 | * For each name/option tuple, if the option is set, override configuration 92 | * value. 93 | */ 94 | function ensure (env, options, names) { 95 | for (let k of Object.keys(names)) { 96 | let v = names[k] 97 | 98 | if (options[v]) { 99 | env[k] = options[v] 100 | env[k + 'Cwd'] = true 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/environment.js: -------------------------------------------------------------------------------- 1 | import { is } from './utils' 2 | import * as errors from './errors' 3 | import { EventEmitter } from 'events' 4 | import fs from 'fs' 5 | import path from 'path' 6 | import yaml from 'js-yaml' 7 | import converter from 'sass-convert' 8 | 9 | export default class Environment extends EventEmitter { 10 | 11 | /** 12 | * @param {Logger} logger 13 | * @param {Boolean} strict 14 | */ 15 | constructor (logger, verbose = false, strict = false) { 16 | super() 17 | 18 | this.logger = logger 19 | this.verbose = verbose 20 | this.strict = strict 21 | 22 | this.on('error', error => { 23 | let friendlyErrors = [ 24 | errors.SassDocError, 25 | converter.BinaryError, 26 | converter.VersionError, 27 | ] 28 | 29 | if (Array.find(friendlyErrors, c => error instanceof c)) { 30 | logger.error(error.message) 31 | } else { 32 | if (is.error(error) && 'stack' in error) { 33 | logger.error(error.stack) 34 | } else { 35 | logger.error(error) 36 | } 37 | } 38 | }) 39 | 40 | if (strict) { 41 | this.on('warning', warning => this.emit('error', warning)) 42 | } else { 43 | this.on('warning', warning => logger.warn(warning.message)) 44 | } 45 | } 46 | 47 | /** 48 | * @param {Object|String} config 49 | */ 50 | load (config) { 51 | if (!config) { 52 | return this.loadDefaultFile() 53 | } 54 | 55 | if (is.string(config)) { 56 | return this.loadFile(config) 57 | } 58 | 59 | if (is.plainObject(config)) { 60 | return this.loadObject(config) 61 | } 62 | 63 | this.emit('error', new errors.SassDocError( 64 | 'Invalid `config` argument, expected string, object or undefined.' 65 | )) 66 | } 67 | 68 | /** 69 | * Merge given configuration object, excluding reserved keys. 70 | * 71 | * @param {Object} config 72 | */ 73 | loadObject (config) { 74 | if (this.file) { 75 | this.file = path.resolve(this.file) 76 | this.dir = path.dirname(this.file) 77 | } 78 | 79 | Object.keys(config) 80 | .filter(key => ['verbose', 'strict'].indexOf(key) === -1) 81 | .forEach(k => { 82 | if (k in this) { 83 | return this.emit('error', new Error( 84 | `Reserved configuration key \`${k}\`.` 85 | )) 86 | } 87 | 88 | this[k] = config[k] 89 | }) 90 | } 91 | 92 | /** 93 | * Get the configuration object from given file. 94 | * 95 | * If the file is not found, emit a warning and fallback to default. 96 | * 97 | * The `dir` property will be the directory of the given file or the CWD 98 | * if no file is given. The configuration paths should be relative to 99 | * it. 100 | * 101 | * The given logger will be injected in the configuration object for 102 | * further usage. 103 | * 104 | * @param {String} file 105 | */ 106 | loadFile (file) { 107 | this.file = file 108 | 109 | if (!this.tryLoadCurrentFile()) { 110 | this.emit('warning', new errors.Warning(`Config file \`${file}\` not found.`)) 111 | this.logger.warn('Falling back to `.sassdocrc`') 112 | this.loadDefaultFile() 113 | } 114 | } 115 | 116 | /** 117 | * Try to load default `.sassdocrc` configuration file, or fallback 118 | * to an empty object. 119 | */ 120 | loadDefaultFile () { 121 | this.file = '.sassdocrc' 122 | this.tryLoadCurrentFile() 123 | } 124 | 125 | /** 126 | * Post process the configuration to ensure `package` and `theme` 127 | * have uniform values. 128 | * 129 | * The `package` key is ensured to be an object. If it's a string, it's 130 | * required as JSON, relative to the configuration file directory. 131 | * 132 | * The `theme` key, if present and not already a function, will be 133 | * resolved to the actual theme function. 134 | */ 135 | postProcess () { 136 | if (!this.dir) { 137 | this.dir = process.cwd() 138 | } 139 | 140 | if (!this.dest) { 141 | this.dest = 'sassdoc' 142 | this.destCwd = true 143 | } 144 | 145 | this.dest = this.resolve(this.dest, this.destCwd) 146 | this.displayDest = path.relative(process.cwd(), this.dest) 147 | 148 | if (!this.package) { 149 | this.defaultPackage() 150 | } 151 | 152 | if (typeof this.package !== 'object') { 153 | this.loadPackage() 154 | } 155 | 156 | if (typeof this.theme !== 'function') { 157 | this.loadTheme() 158 | } 159 | } 160 | 161 | /** 162 | * Process `this.package`. 163 | */ 164 | loadPackage () { 165 | let file = this.resolve(this.package) 166 | this.package = this.tryParseFile(file) 167 | 168 | if (this.package) { 169 | return 170 | } 171 | 172 | this.emit('warning', new errors.Warning(`Package file \`${file}\` not found.`)) 173 | this.logger.warn('Falling back to `package.json`.') 174 | 175 | this.defaultPackage() 176 | } 177 | 178 | /** 179 | * Load `package.json`. 180 | */ 181 | defaultPackage () { 182 | let file = this.resolve('package.json') 183 | this.package = this.tryParseFile(file) 184 | 185 | if (this.package) { 186 | return 187 | } 188 | 189 | this.logger.warn('No package information.') 190 | this.package = {} 191 | } 192 | 193 | /** 194 | * Process `this.theme`. 195 | */ 196 | loadTheme () { 197 | if (this.theme === undefined) { 198 | return this.defaultTheme() 199 | } 200 | 201 | const hasSlash = this.theme.includes('/') 202 | const isScoped = this.theme.startsWith('@') && hasSlash 203 | 204 | // We assume it's a full scoped package name. 205 | if (isScoped) { 206 | return this.tryTheme(this.theme) 207 | } 208 | 209 | // We assume it's a theme name shorthand. 210 | if (!hasSlash) { 211 | return this.tryTheme(`sassdoc-theme-${this.theme}`) 212 | } 213 | 214 | // We assume it's a path to a local theme. 215 | let theme = this.resolve(this.theme, this.themeCwd) 216 | this.themeName = this.theme 217 | this.displayTheme = path.relative(process.cwd(), theme) 218 | 219 | return this.tryTheme(theme) 220 | } 221 | 222 | /** 223 | * Try to load given theme module, or fallback to default theme. 224 | * 225 | * @param {String} module 226 | */ 227 | tryTheme (module) { 228 | try { 229 | require.resolve(module) 230 | } catch (err) { 231 | this.emit('warning', new errors.Warning(`Theme \`${this.theme}\` not found.`)) 232 | this.logger.warn('Falling back to default theme.') 233 | return this.defaultTheme() 234 | } 235 | 236 | this.theme = require(module) 237 | let str = Object.prototype.toString 238 | 239 | if (typeof this.theme !== 'function') { 240 | this.emit('error', new errors.SassDocError( 241 | `Given theme is ${str(this.theme)}, expected ${str(str)}.` // eslint-disable-line comma-spacing 242 | )) 243 | 244 | return this.defaultTheme() 245 | } 246 | 247 | if (this.theme.length !== 2) { 248 | this.logger.warn( 249 | `Given theme takes ${this.theme.length} arguments, expected 2.` 250 | ) 251 | } 252 | } 253 | 254 | /** 255 | * Load `sassdoc-theme-default`. 256 | */ 257 | defaultTheme () { 258 | try { 259 | require.resolve('sassdoc-theme-default') 260 | } catch (err) { 261 | this.emit('error', new errors.SassDocError( 262 | 'Holy shit, the default theme was not found!' 263 | )) 264 | } 265 | 266 | this.theme = require('sassdoc-theme-default') 267 | this.themeName = this.displayTheme = 'default' 268 | } 269 | 270 | /** 271 | * Try to load `this.file`, and if not found, return `false`. 272 | * 273 | * @return {Boolean} 274 | */ 275 | tryLoadCurrentFile () { 276 | let config = this.tryParseFile(this.file) 277 | 278 | if (!config) { 279 | return false 280 | } 281 | 282 | this.load(config) 283 | 284 | return true 285 | } 286 | 287 | /** 288 | * Try `this.parseFile` and return `false` if an `ENOENT` error 289 | * is thrown. 290 | * 291 | * Other exceptions are passed to the `error` event. 292 | * 293 | * @param {String} file 294 | * @return {*} 295 | */ 296 | tryParseFile (file) { 297 | try { 298 | return this.parseFile(file) 299 | } catch (e) { 300 | if (e.code !== 'ENOENT') { 301 | return this.emit('error', e) 302 | } 303 | } 304 | 305 | return false 306 | } 307 | 308 | /** 309 | * Load YAML or JSON from given file. 310 | * 311 | * @param {String} file 312 | * @return {*} 313 | */ 314 | parseFile (file) { 315 | return yaml.safeLoad(fs.readFileSync(file, 'utf-8')) 316 | } 317 | 318 | /** 319 | * Resolve given file from `this.dir`. 320 | * 321 | * @param {String} file 322 | * @param {Boolean} cwd - whether it's relative to CWD (like when 323 | * defined in CLI). 324 | * @return {String} 325 | */ 326 | resolve (file, cwd = false) { 327 | return path.resolve(cwd ? process.cwd() : this.dir, file) 328 | } 329 | } 330 | -------------------------------------------------------------------------------- /src/errors.js: -------------------------------------------------------------------------------- 1 | export class SassDocError extends Error { 2 | constructor (message) { 3 | super(message) 4 | this.message = message // rm when native class support. 5 | } 6 | 7 | get name () { 8 | return 'SassDocError' 9 | } 10 | } 11 | 12 | export class Warning extends SassDocError { 13 | constructor (message) { 14 | super(message) 15 | this.message = message // rm when native class support. 16 | } 17 | 18 | get name () { 19 | return 'Warning' 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/exclude.js: -------------------------------------------------------------------------------- 1 | import through from 'through2' 2 | import minimatch from 'minimatch' 3 | 4 | /** 5 | * @param {Array} patterns 6 | * @return {Object} 7 | */ 8 | export default function exclude (patterns) { 9 | return through.obj((file, enc, cb) => { 10 | if (Array.find(patterns, x => minimatch(file.relative, x))) { 11 | return cb() 12 | } 13 | 14 | cb(null, file) 15 | }) 16 | } 17 | -------------------------------------------------------------------------------- /src/logger.js: -------------------------------------------------------------------------------- 1 | import { is } from './utils' 2 | import * as errors from './errors' 3 | import { format as fmt } from 'util' 4 | import chalkModule from 'chalk' 5 | import style from 'ansi-styles'; 6 | 7 | const chalk = new chalkModule.constructor({ 8 | enabled: process.stderr && process.stderr.isTTY, 9 | }) 10 | 11 | // Special chars. 12 | const chevron = '\xBB' 13 | const checkmark = '\u2713' 14 | const green = chalk.green(chevron) 15 | const yellow = chalk.yellow(chevron) 16 | const red = chalk.red(chevron) 17 | 18 | export default class Logger { 19 | constructor (verbose = false, debug = false) { 20 | this.verbose = verbose 21 | this._stderr = process.stderr 22 | this._debug = debug 23 | this._times = [] 24 | } 25 | 26 | /** 27 | * Log arguments into stderr if the verbose mode is enabled. 28 | */ 29 | log (...args) { 30 | if (this.verbose) { 31 | let str = fmt(`${green} ${args.shift()}`, ...args) 32 | this._stderr.write(`${str}\n`) 33 | } 34 | } 35 | 36 | /** 37 | * Always log arguments as warning into stderr. 38 | */ 39 | warn (...args) { 40 | let str = fmt(`${yellow} [WARNING] ${args.shift()}`, ...args) 41 | this._stderr.write(`${str}\n`) 42 | } 43 | 44 | /** 45 | * Always log arguments as error into stderr. 46 | */ 47 | error (...args) { 48 | let str = fmt(`${red} [ERROR] ${args.shift()}`, ...args) 49 | this._stderr.write(`${str}\n`) 50 | } 51 | 52 | /** 53 | * Init a new timer. 54 | * @param {String} label 55 | */ 56 | time (label) { 57 | this._times[label] = Date.now() 58 | } 59 | 60 | /** 61 | * End timer and log result into stderr. 62 | * @param {String} label 63 | * @param {String} format 64 | */ 65 | timeEnd (label, format = '%s: %dms') { 66 | if (!this.verbose) { 67 | return 68 | } 69 | 70 | let time = this._times[label] 71 | if (!time) { 72 | throw new Error(`No such label: ${label}`) 73 | } 74 | 75 | let duration = Date.now() - time 76 | 77 | let str = fmt(`${chalk.green(checkmark)} ${format}`, label, duration) 78 | this._stderr.write(`${str}\n`) 79 | } 80 | 81 | /** 82 | * Log arguments into stderr if debug mode is enabled (will call all 83 | * argument functions to allow "lazy" arguments). 84 | */ 85 | debug (...args) { 86 | if (!this._debug) { 87 | return 88 | } 89 | 90 | args = args.map(f => { 91 | if (f instanceof Function) { 92 | return f() 93 | } 94 | 95 | return f 96 | }) 97 | 98 | let str = fmt( 99 | `${style.grey.open}${chevron} [DEBUG] ${args.shift()}`, 100 | ...args, 101 | style.grey.close 102 | ) 103 | 104 | this._stderr.write(`${str}\n`) 105 | } 106 | } 107 | 108 | export var empty = { 109 | log: () => {}, 110 | warn: () => {}, 111 | error: () => {}, 112 | debug: () => {}, 113 | } 114 | 115 | /** 116 | * Checks if given object looks like a logger. 117 | * 118 | * If the `debug` function is missing (like for the `console` object), 119 | * it will be set to an empty function in a newly returned object. 120 | * 121 | * If any other method is missing, an exception is thrown. 122 | * 123 | * @param {Object} logger 124 | * @return {Logger} 125 | * @throws {SassDocError} 126 | */ 127 | export function checkLogger (logger) { 128 | const methods = ['log', 'warn', 'error'] 129 | .filter(x => !(x in logger) || !is.function(logger[x])) 130 | 131 | if (methods.length) { 132 | const missing = `"${methods.join('\`, \`')}"` 133 | const s = methods.length > 1 ? 's' : '' 134 | 135 | throw new errors.SassDocError(`Invalid logger, missing ${missing} method${s}`) 136 | } 137 | 138 | if ('debug' in logger) { 139 | return logger 140 | } 141 | 142 | return { 143 | log: logger.log, 144 | warn: logger.warn, 145 | error: logger.error, 146 | debug: empty.debug, 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /src/notifier.js: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk' 2 | 3 | /** 4 | * Sometimes check for update and notify the user. 5 | * 6 | * @param {Object} pkg Package definition. 7 | * @param {Logger} logger 8 | */ 9 | export default function notify (pkg, logger) { 10 | const notifier = require('update-notifier')({ 11 | packageName: pkg.name, 12 | packageVersion: pkg.version, 13 | }) 14 | 15 | if (!notifier.update) { 16 | return 17 | } 18 | 19 | let latest = chalk.yellow(notifier.update.latest) 20 | let current = chalk.grey(`(current: ${notifier.update.current})`) 21 | let command = chalk.blue(`npm update -g ${pkg.name}`) 22 | 23 | logger.log(`Update available: ${latest} ${current}`) 24 | logger.log(`Run ${command} to update.`) 25 | } 26 | -------------------------------------------------------------------------------- /src/parser.js: -------------------------------------------------------------------------------- 1 | import { defer } from './utils' 2 | import * as errors from './errors' 3 | import AnnotationsApi from './annotation' 4 | import sorter from './sorter' 5 | import ScssCommentParser from 'scss-comment-parser' 6 | import through from 'through2' 7 | import concat from 'concat-stream' 8 | import path from 'path' 9 | 10 | export default class Parser { 11 | constructor (env, additionalAnnotations) { 12 | this.annotations = new AnnotationsApi(env) 13 | this.annotations.addAnnotations(additionalAnnotations) 14 | this.scssParser = new ScssCommentParser(this.annotations.list, env) 15 | this.includeUnknownContexts = env.theme && env.theme.includeUnknownContexts 16 | 17 | this.scssParser.commentParser.on('warning', warning => { 18 | env.emit('warning', new errors.Warning(warning.message)) 19 | }) 20 | } 21 | 22 | parse (code, id) { 23 | return this.scssParser.parse(code, id) 24 | } 25 | 26 | /** 27 | * Invoke the `resolve` function of an annotation if present. 28 | * Called with all found annotations except with type "unknown". 29 | */ 30 | postProcess (data) { 31 | data = sorter(data) 32 | let promises = [] 33 | 34 | Object.keys(this.annotations.list).forEach(key => { 35 | let annotation = this.annotations.list[key] 36 | 37 | if (annotation.resolve) { 38 | let promise = Promise.resolve(annotation.resolve(data)) 39 | promises.push(promise) 40 | } 41 | }) 42 | 43 | return Promise.all(promises).then(() => data) 44 | } 45 | 46 | /** 47 | * Return a transform stream meant to be piped in a stream of SCSS 48 | * files. Each file will be passed-through as-is, but they are all 49 | * parsed to generate a SassDoc data array. 50 | * 51 | * The returned stream has an additional `promise` property, containing 52 | * a `Promise` object that will be resolved when the stream is done and 53 | * the data is fulfiled. 54 | * 55 | * @param {Object} parser 56 | * @return {Object} 57 | */ 58 | stream () { 59 | let deferred = defer() 60 | let data = [] 61 | 62 | let transform = (file, enc, cb) => { 63 | 64 | let parseFile = ({ buf, name, path }) => { 65 | let fileData = this.parse(buf.toString(enc), name) 66 | 67 | fileData.forEach(item => { 68 | item.file = { 69 | path, 70 | name, 71 | } 72 | 73 | data.push(item) 74 | }) 75 | } 76 | 77 | let parseError = false; 78 | try { 79 | if (file.isBuffer()) { 80 | let args = { 81 | buf: file.contents, 82 | name: path.basename(file.relative), 83 | path: file.relative, 84 | } 85 | parseFile(args) 86 | } 87 | 88 | if (file.isStream()) { 89 | file.pipe(concat(buf => { 90 | parseFile({ buf }) 91 | })) 92 | } 93 | } catch (error) { 94 | parseError = true; 95 | cb(error); 96 | } 97 | 98 | // Pass-through. 99 | if (!parseError) { 100 | cb(null, file) 101 | } 102 | } 103 | 104 | let flush = cb => { 105 | if (!this.includeUnknownContexts) { 106 | data = data.filter(item => item.context.type !== 'unknown') 107 | } 108 | this.postProcess(data).then(processed => { 109 | data = processed; 110 | deferred.resolve(data) 111 | cb() 112 | }) 113 | } 114 | 115 | let filter = through.obj(transform, flush) 116 | filter.promise = deferred.promise 117 | 118 | return filter 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /src/recurse.js: -------------------------------------------------------------------------------- 1 | import { is } from './utils' 2 | import path from 'path' 3 | import through from 'through2' 4 | import vfs from 'vinyl-fs' 5 | 6 | /** 7 | * Return a transform stream recursing through directory to yield 8 | * Sass/SCSS files instead. 9 | * 10 | * @return {Object} 11 | */ 12 | export default function recurse () { 13 | return through.obj(function (file, enc, cb) { 14 | if (!is.vinylFile(file)) { 15 | // Don't know how to handle this object. 16 | return cb(new Error('Unsupported stream object. Vinyl file expected.')) 17 | } 18 | 19 | if (file.isBuffer() || file.isStream()) { 20 | // Pass-through. 21 | return cb(null, file) 22 | } 23 | 24 | if (!file.isDirectory()) { 25 | // At that stage we want only dirs. Dismiss file.isNull. 26 | return cb() 27 | } 28 | 29 | // It's a directory, find inner Sass/SCSS files. 30 | let pattern = path.resolve(file.path, '**/*.+(sass|scss)') 31 | 32 | vfs.src(pattern) 33 | .pipe(through.obj((file, enc, cb) => { 34 | // Append to "parent" stream. 35 | this.push(file) 36 | cb() 37 | }, () => { 38 | // All done. 39 | cb() 40 | })) 41 | }) 42 | } 43 | -------------------------------------------------------------------------------- /src/sassdoc.js: -------------------------------------------------------------------------------- 1 | import { denodeify, is, g2b } from './utils' 2 | 3 | import Environment from './environment' 4 | import Logger, { checkLogger } from './logger' 5 | import Parser from './parser' 6 | import * as errors from './errors' 7 | import sorter from './sorter' 8 | import exclude from './exclude' 9 | import recurse from './recurse' 10 | 11 | import fs from 'fs' 12 | import path from 'path' 13 | import difference from 'lodash.difference' 14 | import safeWipe from 'safe-wipe' 15 | import vfs from 'vinyl-fs' 16 | import converter from 'sass-convert' 17 | import pipe from 'multipipe' 18 | import through from 'through2' 19 | import mkdir from 'mkdirp'; 20 | 21 | /** 22 | * Expose lower API blocks. 23 | */ 24 | export { Environment, Logger, Parser, sorter, errors } 25 | 26 | /** 27 | * Boostrap Parser and AnnotationsApi, execute parsing phase. 28 | * @return {Stream} 29 | * @return {Promise} - as a property of Stream. 30 | */ 31 | export function parseFilter (env = {}) { 32 | env = ensureEnvironment(env) 33 | 34 | let parser = new Parser(env, env.theme && env.theme.annotations) 35 | 36 | return parser.stream() 37 | } 38 | 39 | /** 40 | * Ensure a proper Environment Object and events. 41 | * @param {Object} config - can be falsy. 42 | * @return {Object} 43 | */ 44 | export function ensureEnvironment (config, onError = e => { throw e }) { 45 | if (config instanceof Environment) { 46 | config.on('error', onError) 47 | return config 48 | } 49 | 50 | let logger = ensureLogger(config) 51 | let env = new Environment(logger, config && config.verbose, config && config.strict) 52 | 53 | env.on('error', onError) 54 | env.load(config) 55 | env.postProcess() 56 | 57 | return env 58 | } 59 | 60 | /** 61 | * @param {Object} config 62 | * @return {Logger} 63 | */ 64 | function ensureLogger (config) { 65 | if (!is.object(config) || !('logger' in config)) { 66 | // Get default logger. 67 | return new Logger(config && config.verbose, process.env.SASSDOC_DEBUG) 68 | } 69 | 70 | let logger = checkLogger(config.logger) 71 | delete config.logger 72 | 73 | return logger 74 | } 75 | 76 | /** 77 | * Default public API method. 78 | * @param {String | Array} src 79 | * @param {Object} env 80 | * @return {Promise | Stream} 81 | * @see srcEnv 82 | */ 83 | export default function sassdoc (...args) { 84 | return srcEnv(documentize, stream)(...args) 85 | 86 | /** 87 | * Safely wipe and re-create the destination directory. 88 | * @return {Promise} 89 | */ 90 | function refresh (env) { 91 | return safeWipe(env.dest, { 92 | force: true, 93 | parent: is.string(env.src) || is.array(env.src) ? g2b(env.src) : null, 94 | silent: true, 95 | }) 96 | .then(() => mkdir(env.dest)) 97 | .then(() => { 98 | env.logger.log(`Folder \`${env.displayDest}\` successfully refreshed.`) 99 | }) 100 | .catch(err => { 101 | // Friendly error for already existing directory. 102 | throw new errors.SassDocError(err.message) 103 | }) 104 | } 105 | 106 | /** 107 | * Render theme with parsed data context. 108 | * @return {Promise} 109 | */ 110 | function theme (env) { 111 | let promise = env.theme(env.dest, env) 112 | 113 | if (!is.promise(promise)) { 114 | let type = Object.prototype.toString.call(promise) 115 | throw new errors.Error(`Theme didn't return a promise, got ${type}.`) 116 | } 117 | 118 | return promise 119 | .then(() => { 120 | let displayTheme = env.displayTheme || 'anonymous' 121 | env.logger.log(`Theme \`${displayTheme}\` successfully rendered.`) 122 | }) 123 | } 124 | 125 | /** 126 | * Execute full SassDoc sequence from a source directory. 127 | * @return {Promise} 128 | */ 129 | async function documentize (env) { 130 | init(env) 131 | let data = await baseDocumentize(env) 132 | 133 | try { 134 | await refresh(env) 135 | await theme(env) 136 | okay(env) 137 | } catch (err) { 138 | env.emit('error', err) 139 | throw err 140 | } 141 | 142 | return data 143 | } 144 | 145 | /** 146 | * Execute full SassDoc sequence from a Vinyl files stream. 147 | * @return {Stream} 148 | * @return {Promise} - as a property of Stream. 149 | */ 150 | function stream (env) { 151 | let filter = parseFilter(env) 152 | 153 | filter.promise 154 | .then(data => { 155 | env.logger.log('Sass sources successfully parsed.') 156 | env.data = data 157 | onEmpty(data, env) 158 | }) 159 | 160 | /** 161 | * Returned Promise await the full sequence, 162 | * instead of just the parsing step. 163 | */ 164 | filter.promise = new Promise((resolve, reject) => { 165 | 166 | async function documentize () { 167 | try { 168 | init(env) 169 | await refresh(env) 170 | await theme(env) 171 | okay(env) 172 | resolve() 173 | } catch (err) { 174 | reject(err) 175 | env.emit('error', err) 176 | throw err 177 | } 178 | } 179 | 180 | filter 181 | .on('end', documentize) 182 | .on('error', err => env.emit('error', err)) 183 | .resume() // Drain. 184 | 185 | }) 186 | 187 | return filter 188 | } 189 | } 190 | 191 | /** 192 | * Parse and return data object. 193 | * @param {String | Array} src 194 | * @param {Object} env 195 | * @return {Promise | Stream} 196 | * @see srcEnv 197 | */ 198 | export function parse (...args) { 199 | 200 | return srcEnv(documentize, stream)(...args) 201 | 202 | /** 203 | * @return {Promise} 204 | */ 205 | async function documentize (env) { 206 | let data = await baseDocumentize(env) 207 | 208 | return data 209 | } 210 | 211 | /** 212 | * Don't pass files through, but pass final data at the end. 213 | * @return {Stream} 214 | */ 215 | function stream (env) { 216 | let parseStream = parseFilter(env) 217 | 218 | let filter = through.obj((file, enc, cb) => cb(), function (cb) { 219 | parseStream.promise.then(data => { 220 | this.push(data) 221 | cb() 222 | }, cb) 223 | }) 224 | 225 | return pipe(parseStream, filter) 226 | } 227 | } 228 | 229 | /** 230 | * Source directory fetching and parsing. 231 | */ 232 | async function baseDocumentize (env) { 233 | let filter = parseFilter(env) 234 | 235 | filter.promise 236 | .then(data => { 237 | env.logger.log(`Folder \`${env.src}\` successfully parsed.`) 238 | env.data = data 239 | onEmpty(data, env) 240 | 241 | env.logger.debug(() => { 242 | fs.writeFile( 243 | 'sassdoc-data.json', 244 | JSON.stringify(data, null, 2) + '\n', 245 | err => { 246 | if (err) throw err 247 | } 248 | ) 249 | 250 | return 'Dumping data to `sassdoc-data.json`.' 251 | }) 252 | }) 253 | 254 | let streams = [ 255 | vfs.src(env.src), 256 | recurse(), 257 | exclude(env.exclude || []), 258 | converter({ from: 'sass', to: 'scss' }), 259 | filter 260 | ] 261 | 262 | let pipeline = () => { 263 | return new Promise((resolve, reject) => { 264 | pipe(...streams, err => 265 | err ? reject(err) : resolve()) 266 | .resume() // Drain. 267 | }) 268 | } 269 | 270 | try { 271 | await pipeline() 272 | await filter.promise 273 | } catch (err) { 274 | env.emit('error', err) 275 | throw err 276 | } 277 | 278 | return env.data 279 | } 280 | 281 | /** 282 | * Return a function taking optional `src` string or array, and optional 283 | * `env` object (arguments are found by their type). 284 | * 285 | * If `src` is set, proxy to `documentize`, otherwise `stream`. 286 | * 287 | * Both functions will be passed the `env` object, which will have a 288 | * `src` key. 289 | */ 290 | function srcEnv (documentize, stream) { 291 | return function (...args) { 292 | let src = Array.find(args, a => is.string(a) || is.array(a)) 293 | let env = Array.find(args, is.plainObject) 294 | 295 | env = ensureEnvironment(env) 296 | 297 | env.logger.debug('process.argv:', () => JSON.stringify(process.argv)) 298 | env.logger.debug('sassdoc version:', () => require('../package.json').version) 299 | env.logger.debug('node version:', () => process.version.substr(1)) 300 | 301 | env.logger.debug('npm version:', () => { 302 | let prefix = path.resolve(process.execPath, '../../lib') 303 | let pkg = path.resolve(prefix, 'node_modules/npm/package.json') 304 | 305 | try { 306 | return require(pkg).version 307 | } catch (e) { 308 | return 'unknown' 309 | } 310 | }) 311 | 312 | env.logger.debug('platform:', () => process.platform) 313 | env.logger.debug('cwd:', () => process.cwd()) 314 | 315 | env.src = src 316 | 317 | env.logger.debug('env:', () => { 318 | let clone = {} 319 | 320 | difference( 321 | Object.getOwnPropertyNames(env), 322 | ['domain', '_events', '_maxListeners', 'logger'] 323 | ) 324 | .forEach(k => clone[k] = env[k]) 325 | 326 | return JSON.stringify(clone, null, 2) 327 | }) 328 | 329 | let task = env.src ? documentize : stream 330 | env.logger.debug('task:', () => env.src ? 'documentize' : 'stream') 331 | 332 | return task(env) 333 | } 334 | } 335 | 336 | /** 337 | * Warn user on empty documentation. 338 | * @param {Array} data 339 | * @param {Object} env 340 | */ 341 | function onEmpty (data, env) { 342 | let message = `SassDoc could not find anything to document.\n 343 | * Are you still using \`/**\` comments ? They're no more supported since 2.0. 344 | See .\n` 345 | 346 | if (!data.length && env.verbose) { 347 | env.emit('warning', new errors.Warning(message)) 348 | } 349 | } 350 | 351 | /** 352 | * Init timer. 353 | * @param {Object} env 354 | */ 355 | function init (env) { 356 | env.logger.time('SassDoc') 357 | } 358 | 359 | /** 360 | * Log final success message. 361 | * @param {Object} env 362 | */ 363 | function okay (env) { 364 | env.logger.log('Process over. Everything okay!') 365 | env.logger.timeEnd('SassDoc', '%s completed after %dms') 366 | } 367 | -------------------------------------------------------------------------------- /src/sorter.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @param {Array} data An array of SassDoc data objects. 3 | * @return {Array} The sorted data. 4 | */ 5 | export default function sort (data) { 6 | return data.sort((a, b) => { 7 | return compare(a.group[0].toLowerCase(), b.group[0].toLowerCase()) || 8 | compare(a.file.path, b.file.path) || 9 | compare(a.context.line.start, b.context.line.start) 10 | }) 11 | } 12 | 13 | function compare (a, b) { 14 | switch (true) { 15 | case a > b: return 1 16 | case a === b: return 0 17 | default: return -1 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | import glob2base from 'glob2base' 2 | import { Glob } from 'glob' 3 | 4 | // Namespace delimiters. 5 | const nsDelimiters = ['::', ':', '\\.', '/'] 6 | const ns = new RegExp(nsDelimiters.join('|'), 'g') 7 | 8 | // Split a namespace on possible namespace delimiters. 9 | export const splitNamespace = value => value.split(ns) 10 | 11 | export function denodeify (fn) { 12 | return function (...args) { 13 | return new Promise((resolve, reject) => { 14 | fn(...args, (err, ...args) => { 15 | if (err) { 16 | reject(err) 17 | return 18 | } 19 | 20 | resolve(...args) 21 | }) 22 | }) 23 | } 24 | } 25 | 26 | export function defer () { 27 | let resolve, reject 28 | 29 | let promise = new Promise((resolve_, reject_) => { 30 | resolve = resolve_ 31 | reject = reject_ 32 | }) 33 | 34 | return { 35 | promise, 36 | resolve, 37 | reject, 38 | } 39 | } 40 | 41 | /** 42 | * Get the base directory of given glob pattern (see `glob2base`). 43 | * 44 | * If it's an array, take the first one. 45 | * 46 | * @param {Array|String} src Glob pattern or array of glob patterns. 47 | * @return {String} 48 | */ 49 | export function g2b (src) { 50 | return glob2base(new Glob([].concat(src)[0])) 51 | } 52 | 53 | /** 54 | * Type checking helpers. 55 | */ 56 | const toString = arg => Object.prototype.toString.call(arg) 57 | 58 | export const is = { 59 | undef: arg => arg === void 0, 60 | string: arg => typeof arg === 'string', 61 | function: arg => typeof arg === 'function', 62 | object: arg => typeof arg === 'object' && arg !== null, 63 | plainObject: arg => toString(arg) === '[object Object]', 64 | array: arg => Array.isArray(arg), 65 | error: arg => is.object(arg) && 66 | (toString(arg) === '[object Error]' || arg instanceof Error), 67 | promise: arg => arg && is.function(arg.then), 68 | stream: arg => arg && is.function(arg.pipe), 69 | vinylFile: arg => is.plainObject(arg) && arg.constructor.name === 'File', 70 | } 71 | -------------------------------------------------------------------------------- /test/annotations/access.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var assert = require('assert') 4 | 5 | describe('#access', function () { 6 | var accessCtor = require('../../dist/annotation/annotations/access').default 7 | var access = accessCtor({}) 8 | 9 | it('should return the trimmed string', function () { 10 | assert.equal(access.parse(' '), '') 11 | assert.equal(access.parse(' '), '') 12 | assert.equal(access.parse('\ntest\t'), 'test') 13 | assert.equal(access.parse('\nte\nst\t'), 'te\nst') 14 | }) 15 | 16 | it('should autofill based on default', function () { 17 | assert.equal(access.autofill({ context: { name: 'non-private' }, access: 'auto' }), 'public') 18 | assert.equal(access.autofill({ context: { name: '_private-name' }, access: 'auto' }), 'private') 19 | assert.equal(access.autofill({ context: { name: '-private-name' }, access: 'auto' }), 'private') 20 | }) 21 | 22 | it('should ignore autofill if privatePrefix is false', function () { 23 | var accessEnv = accessCtor({ privatePrefix: false }) 24 | assert.equal(accessEnv.autofill({ context: { name: 'non-private' }, access: 'auto' }), undefined) 25 | assert.equal(accessEnv.autofill({ context: { name: '_private-name' }, access: 'auto' }), undefined) 26 | assert.equal(accessEnv.autofill({ context: { name: '-private-name' }, access: 'auto' }), undefined) 27 | }) 28 | 29 | it('should autofill based on privatePrefix', function () { 30 | var accessEnv = accessCtor({ privatePrefix: '^--' }) 31 | assert.equal(accessEnv.autofill({ context: { name: '-non-private' }, access: 'auto' }), 'public') 32 | assert.equal(accessEnv.autofill({ context: { name: '_non-private' }, access: 'auto' }), 'public') 33 | assert.equal(accessEnv.autofill({ context: { name: '--private-name' }, access: 'auto' }), 'private') 34 | }) 35 | 36 | it('should respect explicit access', function () { 37 | assert.equal(access.autofill({ context: { name: 'non-private' }, access: 'public' }), undefined) 38 | assert.equal(access.autofill({ context: { name: 'private' }, access: 'private' }), undefined) 39 | assert.equal(access.autofill({ context: { name: '_private-name' }, access: 'auto' }), 'private') 40 | }) 41 | 42 | it('should work when not autofilled', function () { 43 | var data = [{ access: 'auto' }] 44 | access.resolve(data) 45 | assert.equal(data[0].access, 'public') 46 | }) 47 | }) 48 | -------------------------------------------------------------------------------- /test/annotations/alias.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var assert = require('assert') 4 | 5 | describe('#alias', function () { 6 | var aliasCtor = require('../../dist/annotation/annotations/alias').default 7 | var alias = aliasCtor({}) 8 | 9 | it('should return the trimmed string', function () { 10 | assert.equal(alias.parse(' '), '') 11 | assert.equal(alias.parse(' '), '') 12 | assert.equal(alias.parse('\ntest\t'), 'test') 13 | assert.equal(alias.parse('\nte\nst\t'), 'te\nst') 14 | }) 15 | }) 16 | -------------------------------------------------------------------------------- /test/annotations/api.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var assert = require('assert') 4 | 5 | describe('#AnnotationsApi', function () { 6 | var AnnotationsApi = require('../../dist/annotation').default 7 | var api = new AnnotationsApi() 8 | 9 | it('should include the right number of annotations', function () { 10 | assert.equal( 11 | Object.keys(api.list).length, 12 | 22 13 | ) 14 | }) 15 | }) 16 | -------------------------------------------------------------------------------- /test/annotations/author.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var assert = require('assert') 4 | 5 | describe('#author', function () { 6 | var authorCtor = require('../../dist/annotation/annotations/author').default 7 | var author = authorCtor({}) 8 | 9 | it('should return the trimmed string', function () { 10 | assert.equal(author.parse(' '), '') 11 | assert.equal(author.parse(' '), '') 12 | assert.equal(author.parse('\ntest\t'), 'test') 13 | assert.equal(author.parse('\nte\nst\t'), 'te\nst') 14 | }) 15 | }) 16 | -------------------------------------------------------------------------------- /test/annotations/content.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var assert = require('assert') 4 | 5 | describe('#content', function () { 6 | var contentCtor = require('../../dist/annotation/annotations/content').default 7 | var content = contentCtor({}) 8 | 9 | it('should return object', function () { 10 | assert.deepEqual(content.parse('Test'), 'Test') 11 | assert.deepEqual(content.parse('\nTest\t'), 'Test') 12 | assert.deepEqual(content.parse('\nTest\n\nTest\t'), 'Test\n\nTest') 13 | }) 14 | 15 | it('should add @content to all items that contain it in item.context.code', function () { 16 | assert.deepEqual(content.autofill({ context: { code: '@content' } }), '') 17 | }) 18 | }) 19 | -------------------------------------------------------------------------------- /test/annotations/defaults.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var assert = require('assert') 4 | var File = require('vinyl') 5 | var Buffer = require('safe-buffer').Buffer 6 | var sassdoc = require('../../') 7 | 8 | describe('#defaults', function () { 9 | var dummy = {} 10 | 11 | before(function () { 12 | var file = new File({ 13 | path: 'test/fixture/dummy.scss', 14 | contents: new Buffer('/// A dummy function\n@function dummy() {}') 15 | }) 16 | 17 | var stream = sassdoc.parseFilter() 18 | stream.write(file) 19 | stream.end() 20 | 21 | return stream.promise.then(function (data) { 22 | dummy = data[0] 23 | }) 24 | }) 25 | 26 | it('should assign proper default values', function () { 27 | assert.deepEqual(['undefined'], dummy.group) 28 | assert.strictEqual('public', dummy.access) 29 | }) 30 | }) 31 | -------------------------------------------------------------------------------- /test/annotations/deprecated.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var assert = require('assert') 4 | 5 | describe('#deprecated', function () { 6 | var deprecatedCtor = require('../../dist/annotation/annotations/deprecated').default 7 | var deprecated = deprecatedCtor({}) 8 | 9 | it('should return the trimmed string', function () { 10 | assert.equal(deprecated.parse(' '), '') 11 | assert.equal(deprecated.parse(' '), '') 12 | assert.equal(deprecated.parse('\ntest\t'), 'test') 13 | assert.equal(deprecated.parse('\nte\nst\t'), 'te\nst') 14 | }) 15 | }) 16 | -------------------------------------------------------------------------------- /test/annotations/envMock.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | logger: { 3 | warn: function () {}, 4 | log: function () {}, 5 | error: function () {} 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /test/annotations/example.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var assert = require('assert') 4 | 5 | describe('#example', function () { 6 | var exampleCtor = require('../../dist/annotation/annotations/example').default 7 | var example = exampleCtor({}) 8 | 9 | it('default type should be `scss`', function () { 10 | assert.deepEqual(example.parse(''), { type: 'scss', code: '' }) 11 | assert.deepEqual(example.parse('\n'), { type: 'scss', code: '' }) 12 | assert.deepEqual(example.parse('some code'), { type: 'scss', code: 'some code' }) 13 | }) 14 | 15 | it('should remove leading linebreaks', function () { 16 | assert.deepEqual(example.parse('\nsome code\n'), { type: 'scss', code: 'some code' }) 17 | }) 18 | 19 | it('should strip indent', function () { 20 | assert.deepEqual(example.parse('\n some code\n indented\n'), { type: 'scss', code: 'some code\n indented' }) 21 | }) 22 | 23 | it('should extract type and description from first line', function () { 24 | assert.deepEqual(example.parse('type\nsome code'), { type: 'type', code: 'some code' }) 25 | assert.deepEqual(example.parse('type - description\nsome code'), { type: 'type', description: 'description', code: 'some code' }) 26 | assert.deepEqual(example.parse('type description\nsome code'), { type: 'type', description: 'description', code: 'some code' }) 27 | }) 28 | }) 29 | -------------------------------------------------------------------------------- /test/annotations/group.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var assert = require('assert') 4 | 5 | describe('#group', function () { 6 | var groupCtor = require('../../dist/annotation/annotations/group').default 7 | var group = groupCtor({}) 8 | 9 | it('should parse a single group and ingore case', function () { 10 | assert.deepEqual(group.parse('group'), ['group']) 11 | assert.deepEqual(group.parse('GRoup'), ['group']) 12 | }) 13 | 14 | it('should parse a description from subsequent lines', function () { 15 | var item = {} 16 | assert.deepEqual(group.parse('group\ndescription', item), ['group']) 17 | assert.deepEqual(item, {'groupDescriptions': { 18 | 'group': 'description' 19 | }}) 20 | }) 21 | }) 22 | -------------------------------------------------------------------------------- /test/annotations/ignore.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var assert = require('assert') 4 | 5 | describe('#ignore', function () { 6 | var ignoreCtor = require('../../dist/annotation/annotations/ignore').default 7 | var ignore = ignoreCtor({}) 8 | 9 | it('should return nothing', function () { 10 | assert.equal(ignore.parse('\nte\nst\t'), undefined) 11 | assert.equal(ignore.parse(''), undefined) 12 | assert.equal(ignore.parse(), undefined) 13 | }) 14 | }) 15 | -------------------------------------------------------------------------------- /test/annotations/link.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var assert = require('assert') 4 | 5 | describe('#link', function () { 6 | var linkCtor = require('../../dist/annotation/annotations/link').default 7 | var link = linkCtor({}) 8 | 9 | it('should return an object', function () { 10 | assert.deepEqual(link.parse(''), { url: '', caption: '' }) 11 | }) 12 | 13 | it('should work with funny spaces and linebreaks', function () { 14 | assert.deepEqual(link.parse('\t\n\nhttp://sass.com \t\n\n'), { url: 'http://sass.com', caption: '' }) 15 | }) 16 | 17 | it('should return the caption optionally', function () { 18 | assert.deepEqual(link.parse('http://sass.com'), { url: 'http://sass.com', caption: '' }) 19 | assert.deepEqual(link.parse('caption'), { url: '', caption: 'caption' }) 20 | assert.deepEqual(link.parse('http://sass.com caption'), { url: 'http://sass.com', caption: 'caption' }) 21 | assert.deepEqual(link.parse('http://sass.com multiple words caption'), { url: 'http://sass.com', caption: 'multiple words caption' }) 22 | }) 23 | }) 24 | -------------------------------------------------------------------------------- /test/annotations/name.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var assert = require('assert') 4 | 5 | describe('#name', function () { 6 | var nameCtor = require('../../dist/annotation/annotations/name').default 7 | var name = nameCtor({}) 8 | 9 | it('should return the trimmed string', function () { 10 | assert.equal(name.parse(' '), '') 11 | assert.equal(name.parse(' '), '') 12 | assert.equal(name.parse('\ntest\t'), 'test') 13 | assert.equal(name.parse('\nte\nst\t'), 'te\nst') 14 | }) 15 | }) 16 | -------------------------------------------------------------------------------- /test/annotations/output.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var assert = require('assert') 4 | 5 | describe('#output', function () { 6 | var outputCtor = require('../../dist/annotation/annotations/output').default 7 | var output = outputCtor({}) 8 | 9 | it('should parse an output description', function () { 10 | assert.deepEqual(output.parse('position'), 'position') 11 | }) 12 | 13 | it('should parse include linebreaks', function () { 14 | assert.deepEqual(output.parse('one\ntwo\nthree'), 'one\ntwo\nthree') 15 | }) 16 | }) 17 | -------------------------------------------------------------------------------- /test/annotations/parameter.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var assert = require('assert') 4 | 5 | describe('#parameter', function () { 6 | var paramCtor = require('../../dist/annotation/annotations/parameter').default 7 | var param = paramCtor(require('./envMock')) 8 | 9 | it('should return an object', function () { 10 | assert.deepEqual(param.parse('{type} $hyphenated-name [default] - description'), { type: 'type', name: 'hyphenated-name', default: 'default', description: 'description' }) 11 | assert.deepEqual(param.parse('{type} $name [default] - description [with brackets]'), { type: 'type', name: 'name', default: 'default', description: 'description [with brackets]' }) 12 | assert.deepEqual(param.parse('{List} $list - list to check'), { type: 'List', name: 'list', description: 'list to check' }) 13 | }) 14 | 15 | it('should parse all chars in type', function () { 16 | assert.deepEqual(param.parse('{*} $name - description'), { type: '*', name: 'name', description: 'description' }) 17 | assert.deepEqual(param.parse('{type|other} $name - description'), { type: 'type|other', name: 'name', description: 'description' }) 18 | }) 19 | 20 | it('should work for multiline description', function () { 21 | assert.deepEqual(param.parse('{type} $hyphenated-name [default] - description\nmore\nthan\none\nline'), { type: 'type', name: 'hyphenated-name', default: 'default', description: 'description\nmore\nthan\none\nline' }) 22 | }) 23 | 24 | it('should work without the $', function () { 25 | assert.deepEqual(param.parse('{type} hyphenated-name [default] - description\nmore\nthan\none\nline'), { type: 'type', name: 'hyphenated-name', default: 'default', description: 'description\nmore\nthan\none\nline' }) 26 | }) 27 | 28 | it('should work without a type', function () { 29 | assert.deepEqual(param.parse('hyphenated-name [default] - description\nmore\nthan\none\nline'), { name: 'hyphenated-name', default: 'default', description: 'description\nmore\nthan\none\nline' }) 30 | }) 31 | 32 | it('should warn when a name is missing', function (done) { 33 | param = paramCtor({ 34 | logger: { 35 | warn: function (msg) { 36 | assert.equal(msg, '@parameter must at least have a name. Location: FileID:1:2') 37 | done() 38 | } 39 | } 40 | }) 41 | assert.deepEqual(param.parse('{type} [default] - description\nmore\nthan\none\nline', { commentRange: { start: 1, end: 2 } }, 'FileID'), undefined) 42 | }) 43 | 44 | it('should work without a description', function () { 45 | assert.deepEqual(param.parse('{type} name'), { type: 'type', name: 'name' }) 46 | }) 47 | }) 48 | -------------------------------------------------------------------------------- /test/annotations/property.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var assert = require('assert') 4 | 5 | describe('#property', function () { 6 | var propertyCtor = require('../../dist/annotation/annotations/property').default 7 | var prop = propertyCtor({}) 8 | 9 | it('should parse the prop annotation', function () { 10 | assert.deepEqual(prop.parse('base'), { 11 | type: 'Map', 12 | name: 'base' 13 | }) 14 | 15 | assert.deepEqual(prop.parse('{Function} base.default'), { 16 | type: 'Function', 17 | name: 'base.default' 18 | }) 19 | 20 | assert.deepEqual(prop.parse('{Function} base.default - description'), { 21 | type: 'Function', 22 | name: 'base.default', 23 | description: 'description' 24 | }) 25 | 26 | assert.deepEqual(prop.parse('{Function} base.default [default] - description'), { 27 | type: 'Function', 28 | name: 'base.default', 29 | default: 'default', 30 | description: 'description' 31 | }) 32 | 33 | assert.deepEqual(prop.parse('{Function} base.default [default] - description [with brackets]'), { 34 | type: 'Function', 35 | name: 'base.default', 36 | default: 'default', 37 | description: 'description [with brackets]' 38 | }) 39 | }) 40 | 41 | it('should work for multiline description', function () { 42 | assert.deepEqual(prop.parse('{Function} base.default [default] - description\nmore\nthan\none\nline'), { 43 | type: 'Function', 44 | name: 'base.default', 45 | default: 'default', 46 | description: 'description\nmore\nthan\none\nline' 47 | }) 48 | }) 49 | }) 50 | -------------------------------------------------------------------------------- /test/annotations/require.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var assert = require('assert') 4 | 5 | describe('#require', function () { 6 | var requireCtor = require('../../dist/annotation/annotations/require').default 7 | var _require = requireCtor({}) 8 | 9 | it('should default to function', function () { 10 | assert.deepEqual(_require.parse('name - description'), { type: 'function', name: 'name', description: 'description', 'external': false }) 11 | assert.deepEqual(_require.parse('name description'), { type: 'function', name: 'name', description: 'description', 'external': false }) 12 | }) 13 | 14 | it('should work for variables with or without $', function () { 15 | assert.deepEqual(_require.parse('{variable} $my-variable'), { type: 'variable', name: 'my-variable', 'external': false }) 16 | assert.deepEqual(_require.parse('{variable} my-variable'), { type: 'variable', name: 'my-variable', 'external': false }) 17 | }) 18 | 19 | it('should work with optional variable type if $ is used', function () { 20 | assert.deepEqual(_require.parse('{variable} my-variable'), { type: 'variable', name: 'my-variable', 'external': false }) 21 | assert.deepEqual(_require.parse('$my-variable'), { type: 'variable', name: 'my-variable', 'external': false }) 22 | }) 23 | 24 | it('should work for placeholders with or without %', function () { 25 | assert.deepEqual(_require.parse('{placeholder} %my-placeholder'), { type: 'placeholder', name: 'my-placeholder', 'external': false }) 26 | assert.deepEqual(_require.parse('{placeholder} my-placeholder'), { type: 'placeholder', name: 'my-placeholder', 'external': false }) 27 | }) 28 | 29 | it('should work with optional placeholder type if % is used', function () { 30 | assert.deepEqual(_require.parse('{placeholder} my-placeholder'), { type: 'placeholder', name: 'my-placeholder', 'external': false }) 31 | assert.deepEqual(_require.parse('%my-placeholder'), { type: 'placeholder', name: 'my-placeholder', 'external': false }) 32 | }) 33 | 34 | it('should work for external requires', function () { 35 | assert.deepEqual(_require.parse('{variable} extern::lib'), { type: 'variable', name: 'extern::lib', 'external': true }) 36 | assert.deepEqual(_require.parse('extern::lib'), { type: 'function', name: 'extern::lib', 'external': true }) 37 | assert.deepEqual(_require.parse('$extern::lib'), { type: 'variable', name: 'extern::lib', 'external': true }) 38 | }) 39 | 40 | it('should work for external requires with url', function () { 41 | assert.deepEqual(_require.parse('{variable} extern::lib - description '), 42 | { type: 'variable', name: 'extern::lib', 'external': true, 'description': 'description', 'url': 'http://url.com' }) 43 | }) 44 | 45 | it('should work for name and url', function () { 46 | assert.deepEqual(_require.parse('SassCore::map-has-key '), 47 | { type: 'function', name: 'SassCore::map-has-key', 'external': true, 'url': 'http://sass-lang.com/documentation/Sass/Script/Functions.html#map_has_key-instance_method' }) 48 | }) 49 | 50 | it('should work for multiline description', function () { 51 | assert.deepEqual(_require.parse('name - description\nmore\nthan\none\nline'), { type: 'function', name: 'name', description: 'description\nmore\nthan\none\nline', 'external': false }) 52 | }) 53 | 54 | it('should resolve by type and name', function () { 55 | var data = [ 56 | { context: { type: 'function', name: 'rem' } }, 57 | { context: { type: 'mixin', name: 'rem' } }, 58 | { context: { type: 'mixin', name: 'test' }, require: [ { type: 'mixin', name: 'rem' } ] } 59 | ] 60 | 61 | _require.resolve(data) 62 | 63 | assert.deepEqual(data[2].require[0].item.context, { type: 'mixin', name: 'rem' }) 64 | }) 65 | }) 66 | -------------------------------------------------------------------------------- /test/annotations/return.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var assert = require('assert') 4 | 5 | describe('#return', function () { 6 | var returnsCtor = require('../../dist/annotation/annotations/return').default 7 | var returns = returnsCtor(require('./envMock')) 8 | 9 | it('should return an object', function () { 10 | assert.deepEqual(returns.parse('{List} - list to check'), { type: 'List', description: 'list to check' }) 11 | }) 12 | 13 | it('should parse all chars in type', function () { 14 | assert.deepEqual(returns.parse('{*} - description'), { type: '*', description: 'description' }) 15 | assert.deepEqual(returns.parse('{type|other} - description'), { type: 'type|other', description: 'description' }) 16 | }) 17 | 18 | it('should work for multiline description', function () { 19 | assert.deepEqual(returns.parse('{type} - description\nmore\nthan\none\nline'), { type: 'type', description: 'description\nmore\nthan\none\nline' }) 20 | }) 21 | 22 | it('should work without description', function () { 23 | assert.deepEqual(returns.parse('{type}'), { type: 'type' }) 24 | }) 25 | 26 | it('should fail without type', function (done) { 27 | var ret = returnsCtor({ 28 | logger: { 29 | warn: function (msg) { 30 | assert.equal(msg, '@return must at least have a type. Location: FileID:1:2') 31 | done() 32 | } 33 | } 34 | }) 35 | assert.deepEqual(ret.parse('', { commentRange: { start: 1, end: 2 } }, 'FileID'), undefined) 36 | }) 37 | }) 38 | -------------------------------------------------------------------------------- /test/annotations/see.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var assert = require('assert') 4 | 5 | describe('#see', function () { 6 | var seeCtor = require('../../dist/annotation/annotations/see').default 7 | var see = seeCtor({}) 8 | 9 | it('should default to function', function () { 10 | assert.deepEqual(see.parse('name'), { type: 'function', name: 'name' }) 11 | }) 12 | 13 | it('should rewrite the .toJSON method', function () { 14 | var data = [{ description: 'desc', context: { name: 'name' }, group: 'test' }, { see: [see.parse('name')] }] 15 | see.resolve(data) 16 | assert.deepEqual(data[1].see.toJSON(), [{ description: 'desc', context: { name: 'name' }, group: 'test' }]) 17 | }) 18 | }) 19 | -------------------------------------------------------------------------------- /test/annotations/since.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var assert = require('assert') 4 | 5 | describe('#since', function () { 6 | var sinceCtor = require('../../dist/annotation/annotations/since').default 7 | var since = sinceCtor({}) 8 | 9 | it('should return an object', function () { 10 | assert.deepEqual(since.parse(' '), {}) 11 | assert.deepEqual(since.parse('1.5.7'), { version: '1.5.7' }) 12 | assert.deepEqual(since.parse('1.5.7 - here is a description'), { version: '1.5.7', description: 'here is a description' }) 13 | }) 14 | 15 | it('should work for multiline description', function () { 16 | assert.deepEqual(since.parse('1.5.7 - description\nmore\nthan\none\nline'), { version: '1.5.7', description: 'description\nmore\nthan\none\nline' }) 17 | }) 18 | }) 19 | -------------------------------------------------------------------------------- /test/annotations/throw.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var assert = require('assert') 4 | 5 | describe('#throw', function () { 6 | var throwCtor = require('../../dist/annotation/annotations/throw').default 7 | var _throw = throwCtor({}) 8 | 9 | it('should return the trimmed string', function () { 10 | assert.equal(_throw.parse(' '), '') 11 | assert.equal(_throw.parse(' '), '') 12 | assert.equal(_throw.parse('\ntest\t'), 'test') 13 | assert.equal(_throw.parse('\nte\nst\t'), 'te\nst') 14 | }) 15 | }) 16 | -------------------------------------------------------------------------------- /test/annotations/todo.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var assert = require('assert') 4 | 5 | describe('#todo', function () { 6 | var todoCtor = require('../../dist/annotation/annotations/todo').default 7 | var todo = todoCtor({}) 8 | 9 | it('should return the trimmed string', function () { 10 | assert.equal(todo.parse(' '), '') 11 | assert.equal(todo.parse(' '), '') 12 | assert.equal(todo.parse('\ntest\t'), 'test') 13 | assert.equal(todo.parse('\nte\nst\t'), 'te\nst') 14 | }) 15 | }) 16 | -------------------------------------------------------------------------------- /test/annotations/type.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var assert = require('assert') 4 | 5 | describe('#type', function () { 6 | var typeCtor = require('../../dist/annotation/annotations/type').default 7 | var type = typeCtor({}) 8 | 9 | it('should return the trimmed string', function () { 10 | assert.equal(type.parse(' '), '') 11 | assert.equal(type.parse(' '), '') 12 | assert.equal(type.parse('\ntest\t'), 'test') 13 | assert.equal(type.parse('\nte\nst\t'), 'te\nst') 14 | }) 15 | }) 16 | -------------------------------------------------------------------------------- /test/api/documentize.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var fs = require('fs') 4 | var assert = require('assert') 5 | var rimraf = require('rimraf') 6 | var sassdoc = require('../../') 7 | 8 | function clean (done) { 9 | rimraf('sassdoc', done) 10 | } 11 | 12 | function read (filePath) { 13 | return fs.readFileSync(filePath, 'utf8') 14 | } 15 | 16 | describe('#documentize', function () { 17 | before(clean) 18 | after(clean) 19 | 20 | beforeEach(function () { 21 | return sassdoc('./test/data') 22 | }) 23 | 24 | it('should produce documentation files', function () { 25 | assert.ok(fs.existsSync('sassdoc')) 26 | assert.ok(fs.existsSync('sassdoc/index.html')) 27 | assert.ok(fs.existsSync('sassdoc/assets')) 28 | }) 29 | }) 30 | 31 | describe('#documentize-parse', function () { 32 | var expected = read('test/data/expected.json').trim() 33 | var result 34 | 35 | before(function () { 36 | return sassdoc.parse('test/data/test.scss') 37 | .then(function (data) { 38 | result = data 39 | }) 40 | }) 41 | 42 | it('should return a proper data Array', function () { 43 | assert.ok(Array.isArray(result)) 44 | assert.strictEqual(expected, JSON.stringify(result, null, 2)) 45 | }) 46 | }) 47 | -------------------------------------------------------------------------------- /test/api/exclude.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var assert = require('assert') 4 | var vfs = require('vinyl-fs') 5 | var through = require('through2') 6 | var exclude = require('../../dist/exclude').default 7 | 8 | describe('#exclude', function () { 9 | var files = [] 10 | 11 | function inspect () { 12 | return through.obj(function (file, enc, cb) { 13 | files.push(file.relative) 14 | cb(null, file) 15 | }) 16 | } 17 | 18 | before(function (done) { 19 | vfs.src('./test/fixture/**/*.scss') 20 | .pipe(exclude(['two.scss'])) 21 | .pipe(inspect()) 22 | .on('end', done) 23 | .resume() 24 | }) 25 | 26 | it('should properly exlude file patterns', function () { 27 | assert.strictEqual(2, files.length) 28 | assert.strictEqual(-1, files.indexOf('two.css')) 29 | }) 30 | }) 31 | -------------------------------------------------------------------------------- /test/api/parser.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var assert = require('assert') 4 | var sinon = require('sinon') 5 | var mock = require('../mock') 6 | var Environment = require('../../dist/environment').default 7 | var Parser = require('../../dist/parser').default 8 | var vs = require('vinyl-string') 9 | 10 | describe('#parser', function () { 11 | var warnings = [] 12 | var message 13 | var logger 14 | var env 15 | var parser 16 | var spy 17 | 18 | beforeEach(function () { 19 | spy = sinon.spy() 20 | 21 | logger = new mock.Logger(true) 22 | env = new Environment(logger, false) 23 | warnings = logger.output 24 | env.on('warning', spy) 25 | 26 | parser = new Parser(env) 27 | }) 28 | 29 | it('should warn if a single annotation is used more than once', function () { 30 | message = 'Annotation `return` is only allowed once per comment, second value will be ignored' 31 | parser.parse('///desc\n///@return{String}\n///@return{Array}\n@function fail(){}') 32 | 33 | assert.ok(spy.called) 34 | assert.notEqual(-1, warnings[0].indexOf(message)) 35 | }) 36 | 37 | it('should warn if an annotation is used on wrong type', function () { 38 | message = 'Annotation `type` is not allowed on comment from type `function`' 39 | parser.parse('///desc\n///@type{Map}\n@function fail(){}') 40 | 41 | assert.ok(spy.called) 42 | assert.notEqual(-1, warnings[0].indexOf(message)) 43 | }) 44 | 45 | it('should warn if an annotation is unrecognized', function () { 46 | message = 'Parser for annotation `shouldfail` not found' 47 | parser.parse('///desc\n///@shouldfail fail\n@function fail(){}') 48 | 49 | assert.ok(spy.called) 50 | assert.notEqual(-1, warnings[0].indexOf(message)) 51 | }) 52 | 53 | it('should warn if there\'s more than one poster comment per file', function () { 54 | message = 'You can\'t have more than one poster comment' 55 | parser.parse( 56 | '////\n////@group fail\n////\n\n' + 57 | '////\n////@group fail\n////\n\n' + 58 | '/// desc\n@function fail(){}' 59 | ) 60 | 61 | assert.ok(spy.called) 62 | assert.notEqual(-1, warnings[0].indexOf(message)) 63 | }) 64 | 65 | it('should include unknown contexts if requested by theme', function () { 66 | parser.includeUnknownContexts = true 67 | var parseStream = parser.stream() 68 | 69 | vs('///desc\n', { path: 'fake' }).pipe(parseStream) 70 | 71 | return parseStream.promise.then(data => { 72 | assert.equal(data.length, 1) 73 | assert.equal(data[0].context.type, 'unknown') 74 | }) 75 | }) 76 | 77 | it('should include data from async annotation.resolve fns', function () { 78 | var annotation = () => ({ 79 | name: 'async', 80 | parse: raw => raw, 81 | resolve: data => { 82 | return new Promise((resolve, reject) => { 83 | setTimeout(() => { 84 | data.foo = 'bar' 85 | resolve() 86 | }, 10) 87 | }) 88 | } 89 | }) 90 | parser = new Parser(env, [annotation]) 91 | var parseStream = parser.stream() 92 | 93 | vs('///desc\n///@async\n@function pass(){}', { path: 'fake' }).pipe(parseStream) 94 | 95 | return parseStream.promise.then(data => { 96 | assert.equal(data.foo, 'bar') 97 | }) 98 | }) 99 | 100 | it('should throw a catchable error for invalid scss stream', function (done) { 101 | message = 'Parser did not throw a catchable error for invalid scss' 102 | 103 | vs('///invalid\n$%^', { path: 'fake' }).pipe(parser.stream()) 104 | .on('error', () => { 105 | done() 106 | }) 107 | .on('finish', () => { 108 | done(assert.ok(false, message)) 109 | }) 110 | }) 111 | }) 112 | -------------------------------------------------------------------------------- /test/api/recurse.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var assert = require('assert') 4 | var sinon = require('sinon') 5 | var vfs = require('vinyl-fs') 6 | var File = require('vinyl') 7 | var through = require('through2') 8 | var recurse = require('../../dist/recurse').default 9 | 10 | describe('#recurse', function () { 11 | var files = [] 12 | 13 | before(function (done) { 14 | vfs.src('./test/fixture/') 15 | .pipe(recurse()) 16 | .pipe(through.obj(function (file, enc, cb) { 17 | files.push(file.relative) 18 | cb(null, file) 19 | })) 20 | .on('end', done) 21 | .resume() 22 | }) 23 | 24 | it('should properly recurse a given directory', function () { 25 | assert.strictEqual(3, files.length) 26 | assert.deepEqual(files.sort(), [ 'one.scss', 'three.scss', 'two.scss' ]) 27 | }) 28 | }) 29 | 30 | describe('#recurse-null', function () { 31 | var files = [] 32 | var nullFile = new File({ 33 | contents: null 34 | }) 35 | 36 | before(function (done) { 37 | vfs.src('./test/fixture/') 38 | .pipe(through.obj(function (file, enc, cb) { 39 | this.push(nullFile) 40 | cb(null, file) 41 | })) 42 | .pipe(recurse()) 43 | .pipe(through.obj(function (file, enc, cb) { 44 | files.push(file.relative) 45 | cb(null, file) 46 | })) 47 | .on('end', done) 48 | .resume() 49 | }) 50 | 51 | it('should not pass null files through', function () { 52 | assert.strictEqual(3, files.length) 53 | }) 54 | }) 55 | 56 | describe('#recurse-fail', function () { 57 | var spy = sinon.spy() 58 | 59 | before(function (done) { 60 | vfs.src('./test/fixture/') 61 | .pipe(through.obj(function (file, enc, cb) { 62 | this.push({ fail: 'fail' }) 63 | cb() 64 | })) 65 | .pipe(recurse()) 66 | .on('error', function () { 67 | spy() 68 | done() 69 | }) 70 | .resume() 71 | }) 72 | 73 | it('should fail if non Vinyl objects are passed', function () { 74 | assert.ok(spy.called) 75 | }) 76 | }) 77 | -------------------------------------------------------------------------------- /test/api/stream.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var assert = require('assert') 4 | var fs = require('fs') 5 | var vfs = require('vinyl-fs') 6 | var source = require('vinyl-source-stream') 7 | var rimraf = require('rimraf') 8 | var sassdoc = require('../../') 9 | 10 | function clean (done) { 11 | rimraf('sassdoc', done) 12 | } 13 | 14 | function read (filePath) { 15 | return fs.readFileSync(filePath, 'utf8') 16 | } 17 | 18 | describe('#stream-buffer', function () { 19 | before(clean) 20 | after(clean) 21 | 22 | beforeEach(function () { 23 | var stream = sassdoc() 24 | 25 | vfs.src('./test/data/**/*.scss') 26 | .pipe(stream) 27 | 28 | return stream.promise 29 | }) 30 | 31 | it('should produce documentation files', function () { 32 | assert.ok(fs.existsSync('sassdoc')) 33 | assert.ok(fs.existsSync('sassdoc/index.html')) 34 | assert.ok(fs.existsSync('sassdoc/assets')) 35 | }) 36 | }) 37 | 38 | describe('#stream-stream', function () { 39 | before(clean) 40 | after(clean) 41 | 42 | beforeEach(function () { 43 | var stream = sassdoc() 44 | 45 | fs.createReadStream('test/data/test.scss') 46 | .pipe(source()) 47 | .pipe(stream) 48 | 49 | return stream.promise 50 | }) 51 | 52 | it('should produce documentation files', function () { 53 | assert.ok(fs.existsSync('sassdoc')) 54 | assert.ok(fs.existsSync('sassdoc/index.html')) 55 | assert.ok(fs.existsSync('sassdoc/assets')) 56 | }) 57 | }) 58 | 59 | describe('#stream-parse', function () { 60 | var expected = read('test/data/expected.json').trim() 61 | var result 62 | 63 | before(function (done) { 64 | vfs.src('./test/data/**/*.scss') 65 | .pipe(sassdoc.parse()) 66 | .on('data', function (data) { 67 | result = data 68 | done() 69 | }) 70 | }) 71 | 72 | it('should return a proper data Array', function () { 73 | assert.ok(Array.isArray(result)) 74 | assert.strictEqual(expected, JSON.stringify(result, null, 2)) 75 | }) 76 | }) 77 | -------------------------------------------------------------------------------- /test/config-dest.yaml: -------------------------------------------------------------------------------- 1 | dest: custom-sassdoc 2 | -------------------------------------------------------------------------------- /test/config.yaml: -------------------------------------------------------------------------------- 1 | theme: default 2 | -------------------------------------------------------------------------------- /test/data/expected.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "description": "This is a test function aiming at testing:\n- `@param`\n- `@return`\n- `@throw`\n\n", 4 | "commentRange": { 5 | "start": 65, 6 | "end": 75 7 | }, 8 | "context": { 9 | "type": "function", 10 | "name": "function-specific-test", 11 | "code": "", 12 | "line": { 13 | "start": 77, 14 | "end": 77 15 | } 16 | }, 17 | "parameter": [ 18 | { 19 | "type": "*", 20 | "name": "arg", 21 | "description": "Whatever" 22 | }, 23 | { 24 | "type": "List", 25 | "name": "extra-arguments", 26 | "default": "()", 27 | "description": "Extra arguments\n" 28 | } 29 | ], 30 | "return": { 31 | "type": "*", 32 | "description": "Anything\n" 33 | }, 34 | "throw": [ 35 | "This is an error." 36 | ], 37 | "groupDescriptions": { 38 | "test": "This is a group description. It describes the group.\nIt can be split across multiple lines." 39 | }, 40 | "group": [ 41 | "test" 42 | ], 43 | "access": "private", 44 | "file": { 45 | "path": "test.scss", 46 | "name": "test.scss" 47 | }, 48 | "usedBy": [ 49 | { 50 | "description": "This is a test aiming at testing:\n- autofilled `@requires`\n- autofilled `@error`\n- autofilled `@content`\n", 51 | "context": { 52 | "type": "mixin", 53 | "name": "autofill-test", 54 | "code": "\n autofill-test: $autofill-test;\n $call: function-specific-test(alias-test(),alias-test-aliased(),'test', $no);\n $use: $variable-specific-test;\n @include mixin-specific-test;\n @extend %placeholder-specific-test;\n\n @content;\n\n @include autofill-test();\n @error \"This is an autofilled error\";\n", 55 | "line": { 56 | "start": 126, 57 | "end": 137 58 | } 59 | } 60 | }, 61 | { 62 | "description": "This is a test that autofill can be overwritten.\n", 63 | "context": { 64 | "type": "mixin", 65 | "name": "autofill-test-handwritten", 66 | "code": "\n $call: function-specific-test();\n $use: $variable-specific-test;\n @include mixin-specific-test;\n", 67 | "line": { 68 | "start": 146, 69 | "end": 150 70 | } 71 | } 72 | }, 73 | { 74 | "description": "This is a global test aiming at testing:\n- `@access`\n- `@author`\n- `@deprecated`\n- `@example`\n- `@group`\n- `@ignore`\n- `@link`\n- `@requires`\n- `@see`\n- `@since`\n- `@todo`\n\n", 75 | "context": { 76 | "type": "function", 77 | "name": "global-test", 78 | "code": "", 79 | "line": { 80 | "start": 62, 81 | "end": 62 82 | } 83 | } 84 | } 85 | ] 86 | }, 87 | { 88 | "description": "This is a test mixin aiming at testing:\n- `@content`\n- `@param`\n- `@output`\n- `@throw`\n\n", 89 | "commentRange": { 90 | "start": 81, 91 | "end": 94 92 | }, 93 | "context": { 94 | "type": "mixin", 95 | "name": "mixin-specific-test", 96 | "code": "", 97 | "line": { 98 | "start": 96, 99 | "end": 96 100 | } 101 | }, 102 | "content": "Content is parsed and whatever.", 103 | "parameter": [ 104 | { 105 | "type": "Number", 106 | "name": "number", 107 | "default": "42", 108 | "description": "Number" 109 | }, 110 | { 111 | "type": "Arglist", 112 | "name": "extra-arguments", 113 | "description": "Extra arguments\n" 114 | } 115 | ], 116 | "output": "Nothing", 117 | "throw": [ 118 | "This is an error." 119 | ], 120 | "groupDescriptions": { 121 | "test": "This is a group description. It describes the group.\nIt can be split across multiple lines." 122 | }, 123 | "group": [ 124 | "test" 125 | ], 126 | "access": "private", 127 | "file": { 128 | "path": "test.scss", 129 | "name": "test.scss" 130 | }, 131 | "usedBy": [ 132 | { 133 | "description": "This is a test aiming at testing:\n- autofilled `@requires`\n- autofilled `@error`\n- autofilled `@content`\n", 134 | "context": { 135 | "type": "mixin", 136 | "name": "autofill-test", 137 | "code": "\n autofill-test: $autofill-test;\n $call: function-specific-test(alias-test(),alias-test-aliased(),'test', $no);\n $use: $variable-specific-test;\n @include mixin-specific-test;\n @extend %placeholder-specific-test;\n\n @content;\n\n @include autofill-test();\n @error \"This is an autofilled error\";\n", 138 | "line": { 139 | "start": 126, 140 | "end": 137 141 | } 142 | } 143 | }, 144 | { 145 | "description": "This is a test that autofill can be overwritten.\n", 146 | "context": { 147 | "type": "mixin", 148 | "name": "autofill-test-handwritten", 149 | "code": "\n $call: function-specific-test();\n $use: $variable-specific-test;\n @include mixin-specific-test;\n", 150 | "line": { 151 | "start": 146, 152 | "end": 150 153 | } 154 | } 155 | }, 156 | { 157 | "description": "This is a global test aiming at testing:\n- `@access`\n- `@author`\n- `@deprecated`\n- `@example`\n- `@group`\n- `@ignore`\n- `@link`\n- `@requires`\n- `@see`\n- `@since`\n- `@todo`\n\n", 158 | "context": { 159 | "type": "function", 160 | "name": "global-test", 161 | "code": "", 162 | "line": { 163 | "start": 62, 164 | "end": 62 165 | } 166 | } 167 | } 168 | ] 169 | }, 170 | { 171 | "description": "This is a test variable aiming at testing:\n- `@prop`\n- `@type`\n\n", 172 | "commentRange": { 173 | "start": 100, 174 | "end": 107 175 | }, 176 | "context": { 177 | "type": "variable", 178 | "name": "variable-specific-test", 179 | "value": "()", 180 | "scope": "private", 181 | "line": { 182 | "start": 109, 183 | "end": 109 184 | } 185 | }, 186 | "property": [ 187 | { 188 | "type": "String", 189 | "name": "base.first-key", 190 | "default": "\"default\"", 191 | "description": "Description" 192 | }, 193 | { 194 | "type": "String", 195 | "name": "base.second-key", 196 | "default": "42", 197 | "description": "Description" 198 | } 199 | ], 200 | "type": "{*}", 201 | "groupDescriptions": { 202 | "test": "This is a group description. It describes the group.\nIt can be split across multiple lines." 203 | }, 204 | "group": [ 205 | "test" 206 | ], 207 | "access": "private", 208 | "file": { 209 | "path": "test.scss", 210 | "name": "test.scss" 211 | }, 212 | "usedBy": [ 213 | { 214 | "description": "This is a test aiming at testing:\n- autofilled `@requires`\n- autofilled `@error`\n- autofilled `@content`\n", 215 | "context": { 216 | "type": "mixin", 217 | "name": "autofill-test", 218 | "code": "\n autofill-test: $autofill-test;\n $call: function-specific-test(alias-test(),alias-test-aliased(),'test', $no);\n $use: $variable-specific-test;\n @include mixin-specific-test;\n @extend %placeholder-specific-test;\n\n @content;\n\n @include autofill-test();\n @error \"This is an autofilled error\";\n", 219 | "line": { 220 | "start": 126, 221 | "end": 137 222 | } 223 | } 224 | }, 225 | { 226 | "description": "This is a test that autofill can be overwritten.\n", 227 | "context": { 228 | "type": "mixin", 229 | "name": "autofill-test-handwritten", 230 | "code": "\n $call: function-specific-test();\n $use: $variable-specific-test;\n @include mixin-specific-test;\n", 231 | "line": { 232 | "start": 146, 233 | "end": 150 234 | } 235 | } 236 | }, 237 | { 238 | "description": "This is a global test aiming at testing:\n- `@access`\n- `@author`\n- `@deprecated`\n- `@example`\n- `@group`\n- `@ignore`\n- `@link`\n- `@requires`\n- `@see`\n- `@since`\n- `@todo`\n\n", 239 | "context": { 240 | "type": "function", 241 | "name": "global-test", 242 | "code": "", 243 | "line": { 244 | "start": 62, 245 | "end": 62 246 | } 247 | } 248 | } 249 | ] 250 | }, 251 | { 252 | "description": "This is a test placeholder aiming at testing:\n- `@throw`\n\n", 253 | "commentRange": { 254 | "start": 112, 255 | "end": 115 256 | }, 257 | "context": { 258 | "type": "placeholder", 259 | "name": "placeholder-specific-test", 260 | "code": "", 261 | "line": { 262 | "start": 117, 263 | "end": 117 264 | } 265 | }, 266 | "throw": [ 267 | "This is an error." 268 | ], 269 | "groupDescriptions": { 270 | "test": "This is a group description. It describes the group.\nIt can be split across multiple lines." 271 | }, 272 | "group": [ 273 | "test" 274 | ], 275 | "access": "private", 276 | "file": { 277 | "path": "test.scss", 278 | "name": "test.scss" 279 | }, 280 | "usedBy": [ 281 | { 282 | "description": "This is a test aiming at testing:\n- autofilled `@requires`\n- autofilled `@error`\n- autofilled `@content`\n", 283 | "context": { 284 | "type": "mixin", 285 | "name": "autofill-test", 286 | "code": "\n autofill-test: $autofill-test;\n $call: function-specific-test(alias-test(),alias-test-aliased(),'test', $no);\n $use: $variable-specific-test;\n @include mixin-specific-test;\n @extend %placeholder-specific-test;\n\n @content;\n\n @include autofill-test();\n @error \"This is an autofilled error\";\n", 287 | "line": { 288 | "start": 126, 289 | "end": 137 290 | } 291 | } 292 | }, 293 | { 294 | "description": "This is a global test aiming at testing:\n- `@access`\n- `@author`\n- `@deprecated`\n- `@example`\n- `@group`\n- `@ignore`\n- `@link`\n- `@requires`\n- `@see`\n- `@since`\n- `@todo`\n\n", 295 | "context": { 296 | "type": "function", 297 | "name": "global-test", 298 | "code": "", 299 | "line": { 300 | "start": 62, 301 | "end": 62 302 | } 303 | } 304 | } 305 | ] 306 | }, 307 | { 308 | "description": "This is a test aiming at testing:\n- autofilled `@requires`\n- autofilled `@error`\n- autofilled `@content`\n", 309 | "commentRange": { 310 | "start": 121, 311 | "end": 124 312 | }, 313 | "context": { 314 | "type": "mixin", 315 | "name": "autofill-test", 316 | "code": "\n autofill-test: $autofill-test;\n $call: function-specific-test(alias-test(),alias-test-aliased(),'test', $no);\n $use: $variable-specific-test;\n @include mixin-specific-test;\n @extend %placeholder-specific-test;\n\n @content;\n\n @include autofill-test();\n @error \"This is an autofilled error\";\n", 317 | "line": { 318 | "start": 126, 319 | "end": 137 320 | } 321 | }, 322 | "groupDescriptions": { 323 | "test": "This is a group description. It describes the group.\nIt can be split across multiple lines." 324 | }, 325 | "group": [ 326 | "test" 327 | ], 328 | "access": "private", 329 | "content": "", 330 | "require": [ 331 | { 332 | "type": "mixin", 333 | "name": "mixin-specific-test" 334 | }, 335 | { 336 | "type": "function", 337 | "name": "function-specific-test" 338 | }, 339 | { 340 | "type": "function", 341 | "name": "alias-test" 342 | }, 343 | { 344 | "type": "function", 345 | "name": "alias-test-aliased" 346 | }, 347 | { 348 | "type": "placeholder", 349 | "name": "placeholder-specific-test" 350 | }, 351 | { 352 | "type": "variable", 353 | "name": "variable-specific-test" 354 | } 355 | ], 356 | "throw": [ 357 | "This is an autofilled error" 358 | ], 359 | "file": { 360 | "path": "test.scss", 361 | "name": "test.scss" 362 | } 363 | }, 364 | { 365 | "description": "This is a test that autofill can be overwritten.\n", 366 | "commentRange": { 367 | "start": 142, 368 | "end": 144 369 | }, 370 | "context": { 371 | "type": "mixin", 372 | "name": "autofill-test-handwritten", 373 | "code": "\n $call: function-specific-test();\n $use: $variable-specific-test;\n @include mixin-specific-test;\n", 374 | "line": { 375 | "start": 146, 376 | "end": 150 377 | } 378 | }, 379 | "require": [ 380 | { 381 | "type": "variable", 382 | "name": "variable-specific-test" 383 | }, 384 | { 385 | "type": "function", 386 | "name": "function-specific-test", 387 | "external": false 388 | }, 389 | { 390 | "type": "mixin", 391 | "name": "mixin-specific-test", 392 | "external": false 393 | } 394 | ], 395 | "groupDescriptions": { 396 | "test": "This is a group description. It describes the group.\nIt can be split across multiple lines." 397 | }, 398 | "group": [ 399 | "test" 400 | ], 401 | "access": "private", 402 | "file": { 403 | "path": "test.scss", 404 | "name": "test.scss" 405 | } 406 | }, 407 | { 408 | "description": "This is a test that autofill should report not found\n", 409 | "commentRange": { 410 | "start": 152, 411 | "end": 153 412 | }, 413 | "context": { 414 | "type": "mixin", 415 | "name": "autofill-test-not-found", 416 | "code": "\n", 417 | "line": { 418 | "start": 154, 419 | "end": 155 420 | } 421 | }, 422 | "require": [], 423 | "groupDescriptions": { 424 | "test": "This is a group description. It describes the group.\nIt can be split across multiple lines." 425 | }, 426 | "group": [ 427 | "test" 428 | ], 429 | "access": "private", 430 | "file": { 431 | "path": "test.scss", 432 | "name": "test.scss" 433 | } 434 | }, 435 | { 436 | "description": "This is a test function aiming at testing:\n- `@alias`\n\n", 437 | "commentRange": { 438 | "start": 159, 439 | "end": 162 440 | }, 441 | "context": { 442 | "type": "function", 443 | "name": "alias-test", 444 | "code": "", 445 | "line": { 446 | "start": 164, 447 | "end": 164 448 | } 449 | }, 450 | "alias": "alias-test-aliased", 451 | "groupDescriptions": { 452 | "test": "This is a group description. It describes the group.\nIt can be split across multiple lines." 453 | }, 454 | "group": [ 455 | "test" 456 | ], 457 | "access": "private", 458 | "file": { 459 | "path": "test.scss", 460 | "name": "test.scss" 461 | }, 462 | "usedBy": [ 463 | { 464 | "description": "This is a test aiming at testing:\n- autofilled `@requires`\n- autofilled `@error`\n- autofilled `@content`\n", 465 | "context": { 466 | "type": "mixin", 467 | "name": "autofill-test", 468 | "code": "\n autofill-test: $autofill-test;\n $call: function-specific-test(alias-test(),alias-test-aliased(),'test', $no);\n $use: $variable-specific-test;\n @include mixin-specific-test;\n @extend %placeholder-specific-test;\n\n @content;\n\n @include autofill-test();\n @error \"This is an autofilled error\";\n", 469 | "line": { 470 | "start": 126, 471 | "end": 137 472 | } 473 | } 474 | } 475 | ] 476 | }, 477 | { 478 | "description": "This is a test function aiming at testing:\n- `@alias`\n", 479 | "commentRange": { 480 | "start": 166, 481 | "end": 167 482 | }, 483 | "context": { 484 | "type": "function", 485 | "name": "alias-test-aliased", 486 | "code": "", 487 | "line": { 488 | "start": 169, 489 | "end": 169 490 | } 491 | }, 492 | "groupDescriptions": { 493 | "test": "This is a group description. It describes the group.\nIt can be split across multiple lines." 494 | }, 495 | "group": [ 496 | "test" 497 | ], 498 | "access": "private", 499 | "file": { 500 | "path": "test.scss", 501 | "name": "test.scss" 502 | }, 503 | "aliased": [ 504 | "alias-test" 505 | ], 506 | "aliasedGroup": [ 507 | { 508 | "group": [ 509 | "test" 510 | ], 511 | "name": "alias-test" 512 | } 513 | ], 514 | "usedBy": [ 515 | { 516 | "description": "This is a test aiming at testing:\n- autofilled `@requires`\n- autofilled `@error`\n- autofilled `@content`\n", 517 | "context": { 518 | "type": "mixin", 519 | "name": "autofill-test", 520 | "code": "\n autofill-test: $autofill-test;\n $call: function-specific-test(alias-test(),alias-test-aliased(),'test', $no);\n $use: $variable-specific-test;\n @include mixin-specific-test;\n @extend %placeholder-specific-test;\n\n @content;\n\n @include autofill-test();\n @error \"This is an autofilled error\";\n", 521 | "line": { 522 | "start": 126, 523 | "end": 137 524 | } 525 | } 526 | } 527 | ] 528 | }, 529 | { 530 | "description": "This is a test function aiming at testing:\n- `@alias`\n\n", 531 | "commentRange": { 532 | "start": 171, 533 | "end": 174 534 | }, 535 | "context": { 536 | "type": "function", 537 | "name": "alias-test-should-warn", 538 | "code": "", 539 | "line": { 540 | "start": 176, 541 | "end": 176 542 | } 543 | }, 544 | "groupDescriptions": { 545 | "test": "This is a group description. It describes the group.\nIt can be split across multiple lines." 546 | }, 547 | "group": [ 548 | "test" 549 | ], 550 | "access": "private", 551 | "file": { 552 | "path": "test.scss", 553 | "name": "test.scss" 554 | } 555 | }, 556 | { 557 | "description": "This is a test placeholder aiming at testing:\n- `@name`\n", 558 | "commentRange": { 559 | "start": 178, 560 | "end": 180 561 | }, 562 | "context": { 563 | "type": "placeholder", 564 | "name": "placeholder-[blue,green,red]", 565 | "code": "", 566 | "line": { 567 | "start": 182, 568 | "end": 182 569 | } 570 | }, 571 | "groupDescriptions": { 572 | "test": "This is a group description. It describes the group.\nIt can be split across multiple lines." 573 | }, 574 | "group": [ 575 | "test" 576 | ], 577 | "access": "private", 578 | "file": { 579 | "path": "test.scss", 580 | "name": "test.scss" 581 | } 582 | }, 583 | { 584 | "description": "This is a test CSS block.\n", 585 | "commentRange": { 586 | "start": 184, 587 | "end": 185 588 | }, 589 | "context": { 590 | "type": "css", 591 | "name": "data-foo", 592 | "value": "color: red;", 593 | "line": { 594 | "start": 187, 595 | "end": 188 596 | } 597 | }, 598 | "groupDescriptions": { 599 | "test": "This is a group description. It describes the group.\nIt can be split across multiple lines." 600 | }, 601 | "group": [ 602 | "test" 603 | ], 604 | "access": "private", 605 | "file": { 606 | "path": "test.scss", 607 | "name": "test.scss" 608 | } 609 | }, 610 | { 611 | "description": "This is a global test aiming at testing:\n- `@access`\n- `@author`\n- `@deprecated`\n- `@example`\n- `@group`\n- `@ignore`\n- `@link`\n- `@requires`\n- `@see`\n- `@since`\n- `@todo`\n\n", 612 | "commentRange": { 613 | "start": 10, 614 | "end": 60 615 | }, 616 | "context": { 617 | "type": "function", 618 | "name": "global-test", 619 | "code": "", 620 | "line": { 621 | "start": 62, 622 | "end": 62 623 | } 624 | }, 625 | "access": "public", 626 | "author": [ 627 | "Pascal **Duez**", 628 | "Valérian **Galliat**", 629 | "Kitty **Giraudel**", 630 | "Fabrice **Weinberg**" 631 | ], 632 | "deprecated": "This is a *deprecation* message.", 633 | "example": [ 634 | { 635 | "type": "scss", 636 | "code": "$test: function-global-test();", 637 | "description": "Example description" 638 | } 639 | ], 640 | "group": [ 641 | "test-function" 642 | ], 643 | "ignore": [], 644 | "link": [ 645 | { 646 | "url": "http://google.com", 647 | "caption": "Google" 648 | }, 649 | { 650 | "url": "http://sassdoc.com", 651 | "caption": "" 652 | } 653 | ], 654 | "require": [ 655 | { 656 | "type": "function", 657 | "name": "function-specific-test", 658 | "external": false, 659 | "description": "This is a description with a dash." 660 | }, 661 | { 662 | "type": "mixin", 663 | "name": "mixin-specific-test", 664 | "external": false, 665 | "description": "This is a description with no dash." 666 | }, 667 | { 668 | "type": "variable", 669 | "name": "variable-specific-test", 670 | "external": false 671 | }, 672 | { 673 | "type": "placeholder", 674 | "name": "placeholder-specific-test", 675 | "external": false, 676 | "description": "This is a description and a link. http://sassdoc.com" 677 | }, 678 | { 679 | "type": "function", 680 | "name": "this::is::an::external::dependancy", 681 | "external": true 682 | }, 683 | { 684 | "type": "function", 685 | "name": "this:is:an:external:dependancy", 686 | "external": true 687 | }, 688 | { 689 | "type": "function", 690 | "name": "this/is/an/external/dependancy", 691 | "external": true 692 | }, 693 | { 694 | "type": "function", 695 | "name": "this.is.an.external.dependancy", 696 | "external": true 697 | } 698 | ], 699 | "see": [ 700 | { 701 | "description": "This is a test function aiming at testing:\n- `@param`\n- `@return`\n- `@throw`\n\n", 702 | "context": { 703 | "type": "function", 704 | "name": "function-specific-test", 705 | "code": "", 706 | "line": { 707 | "start": 77, 708 | "end": 77 709 | } 710 | }, 711 | "group": [ 712 | "test" 713 | ] 714 | } 715 | ], 716 | "since": [ 717 | { 718 | "version": "2.0.0", 719 | "description": "Major refacto" 720 | }, 721 | { 722 | "version": "1.0.0", 723 | "description": "Other stuff\n" 724 | } 725 | ], 726 | "todo": [ 727 | "Nothing to do here.", 728 | "My people need me." 729 | ], 730 | "groupDescriptions": { 731 | "test": "This is a group description. It describes the group.\nIt can be split across multiple lines." 732 | }, 733 | "file": { 734 | "path": "test.scss", 735 | "name": "test.scss" 736 | } 737 | } 738 | ] 739 | -------------------------------------------------------------------------------- /test/data/stream: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var path = require('path') 4 | var input = path.resolve(process.argv[2]) 5 | 6 | console.log( 7 | JSON.stringify( 8 | require(input).map(function (item) { 9 | item.file = {} 10 | return item 11 | }), 12 | null, 13 | 2 14 | ) 15 | ) 16 | -------------------------------------------------------------------------------- /test/data/test.scss: -------------------------------------------------------------------------------- 1 | //// 2 | /// This is a top level annotation, acting for the whole file. 3 | /// This description is not parsed at this point but might be in the future. 4 | /// @group test 5 | /// This is a group description. It describes the group. 6 | /// It can be split across multiple lines. 7 | /// @access private 8 | //// 9 | 10 | /// This is a global test aiming at testing: 11 | /// - `@access` 12 | /// - `@author` 13 | /// - `@deprecated` 14 | /// - `@example` 15 | /// - `@group` 16 | /// - `@ignore` 17 | /// - `@link` 18 | /// - `@requires` 19 | /// - `@see` 20 | /// - `@since` 21 | /// - `@todo` 22 | /// 23 | /// @access public 24 | /// 25 | /// @author Pascal **Duez** 26 | /// @author Valérian **Galliat** 27 | /// @author Kitty **Giraudel** 28 | /// @author Fabrice **Weinberg** 29 | /// 30 | /// @deprecated This is a *deprecation* message. 31 | /// 32 | /// @example scss - Example description 33 | /// $test: function-global-test(); 34 | /// 35 | /// @group test-function 36 | /// 37 | /// @ignore Ignore this line. 38 | /// 39 | /// @link http://google.com Google 40 | /// @link http://sassdoc.com 41 | /// 42 | /// @requires function-specific-test - This is a description with a dash. 43 | /// @requires {mixin} mixin-specific-test This is a description with no dash. 44 | /// @requires $variable-specific-test 45 | /// @requires %placeholder-specific-test This is a description and a link. http://sassdoc.com 46 | /// @requires this::is::an::external::dependancy - External dependency with double colon http://github.com 47 | /// @requires this:is:an:external:dependancy - External dependency with single colon http://github.com 48 | /// @requires this/is/an/external/dependancy - External dependency with slash http://github.com 49 | /// @requires this.is.an.external.dependancy - External dependency with dot http://github.com 50 | /// 51 | /// @see function-specific-test 52 | /// @see {mixin} mixin-global-test 53 | /// @see $variable-global-test 54 | /// @see %placeholder-global-test 55 | /// 56 | /// @since 2.0.0 - Major refacto 57 | /// @since 1.0.0 Other stuff 58 | /// 59 | /// @todo Nothing to do here. 60 | /// @todo My people need me. 61 | 62 | @function global-test() {} 63 | 64 | 65 | /// This is a test function aiming at testing: 66 | /// - `@param` 67 | /// - `@return` 68 | /// - `@throw` 69 | /// 70 | /// @param {*} $arg - Whatever 71 | /// @param {List} $extra-arguments [()] - Extra arguments 72 | /// 73 | /// @return {*} Anything 74 | /// 75 | /// @throw This is an error. 76 | 77 | @function function-specific-test() {} 78 | 79 | 80 | 81 | /// This is a test mixin aiming at testing: 82 | /// - `@content` 83 | /// - `@param` 84 | /// - `@output` 85 | /// - `@throw` 86 | /// 87 | /// @content Content is parsed and whatever. 88 | /// 89 | /// @param {Number} $number [42] - Number 90 | /// @param {Arglist} $extra-arguments - Extra arguments 91 | /// 92 | /// @output Nothing 93 | /// 94 | /// @throw This is an error. 95 | 96 | @mixin mixin-specific-test {} 97 | 98 | 99 | 100 | /// This is a test variable aiming at testing: 101 | /// - `@prop` 102 | /// - `@type` 103 | /// 104 | /// @prop {String} base.first-key ["default"] - Description 105 | /// @prop {String} base.second-key [42] - Description 106 | /// 107 | /// @type {*} 108 | 109 | $variable-specific-test: (); 110 | 111 | 112 | /// This is a test placeholder aiming at testing: 113 | /// - `@throw` 114 | /// 115 | /// @throw This is an error. 116 | 117 | %placeholder-specific-test {} 118 | 119 | 120 | 121 | /// This is a test aiming at testing: 122 | /// - autofilled `@requires` 123 | /// - autofilled `@error` 124 | /// - autofilled `@content` 125 | 126 | @mixin autofill-test { 127 | autofill-test: $autofill-test; 128 | $call: function-specific-test(alias-test(),alias-test-aliased(),'test', $no); 129 | $use: $variable-specific-test; 130 | @include mixin-specific-test; 131 | @extend %placeholder-specific-test; 132 | 133 | @content; 134 | 135 | @include autofill-test(); 136 | @error "This is an autofilled error"; 137 | } 138 | 139 | 140 | 141 | 142 | /// This is a test that autofill can be overwritten. 143 | /// @require {function} function-specific-test 144 | /// @require {mixin} mixin-specific-test 145 | 146 | @mixin autofill-test-handwritten { 147 | $call: function-specific-test(); 148 | $use: $variable-specific-test; 149 | @include mixin-specific-test; 150 | } 151 | 152 | /// This is a test that autofill should report not found 153 | /// @require {function} function-not-found 154 | @mixin autofill-test-not-found { 155 | } 156 | 157 | 158 | 159 | /// This is a test function aiming at testing: 160 | /// - `@alias` 161 | /// 162 | /// @alias alias-test-aliased 163 | 164 | @function alias-test() {} 165 | 166 | /// This is a test function aiming at testing: 167 | /// - `@alias` 168 | 169 | @function alias-test-aliased() {} 170 | 171 | /// This is a test function aiming at testing: 172 | /// - `@alias` 173 | /// 174 | /// @alias should-warn 175 | 176 | @function alias-test-should-warn() {} 177 | 178 | /// This is a test placeholder aiming at testing: 179 | /// - `@name` 180 | /// @name placeholder-[blue,green,red] 181 | 182 | %placeholder-#{$color} {} 183 | 184 | /// This is a test CSS block. 185 | /// @name data-foo 186 | 187 | [data-foo="bar"] { color: red; } 188 | -------------------------------------------------------------------------------- /test/env/environment.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var fs = require('fs') 4 | var mkdirp = require('mkdirp') 5 | var path = require('path') 6 | var assert = require('assert') 7 | var sinon = require('sinon') 8 | var rimraf = require('rimraf') 9 | var mock = require('../mock') 10 | var Environment = require('../../dist/environment').default 11 | var ensureEnvironment = require('../../').ensureEnvironment 12 | var loggerModule = require('../../dist/logger') 13 | var Logger = loggerModule.default 14 | Logger.empty = loggerModule.empty 15 | 16 | describe('#environment', function () { 17 | var warnings = [] 18 | var logger 19 | var env 20 | 21 | beforeEach(function () { 22 | logger = new mock.Logger(true) 23 | env = new Environment(logger) 24 | warnings = logger.output 25 | }) 26 | 27 | /** 28 | * Passed in config file. 29 | */ 30 | describe('#config', function () { 31 | var configPath = path.join(__dirname, '../fixture/config.json') 32 | var configFile = require(configPath) 33 | 34 | beforeEach(function () { 35 | env.load(configPath) 36 | env.postProcess() 37 | }) 38 | 39 | it('should properly process a passed in config file', function () { 40 | assert.ok(path.basename(env.file) === 'config.json') 41 | assert.deepEqual(env.display, configFile.display) 42 | }) 43 | }) 44 | 45 | /** 46 | * Passed in wrong type config file. 47 | */ 48 | describe('#config-fail', function () { 49 | var spy 50 | 51 | beforeEach(function () { 52 | spy = sinon.spy() 53 | env.on('error', spy) 54 | }) 55 | 56 | it('should error if config is a number', function () { 57 | env.load(123) 58 | assert.ok(spy.called) 59 | }) 60 | 61 | it('should error if config is an array', function () { 62 | env.load([]) 63 | assert.ok(spy.called) 64 | }) 65 | }) 66 | 67 | /** 68 | * Passed in undefined config file. 69 | */ 70 | describe('#config-fail', function () { 71 | beforeEach(function () { 72 | env.load('fail.json') 73 | env.postProcess() 74 | }) 75 | 76 | it('should warn if config file is not found', function () { 77 | assert.ok(path.basename(env.file) === '.sassdocrc') 78 | assert.notEqual(-1, warnings[0].indexOf('Config file `fail.json` not found')) 79 | assert.notEqual(-1, warnings[1].indexOf('Falling back to `.sassdocrc')) 80 | }) 81 | }) 82 | 83 | /** 84 | * Passed a config file with reserved keys. 85 | */ 86 | describe('#config-fail', function () { 87 | var spy = sinon.spy() 88 | 89 | beforeEach(function () { 90 | env.on('error', spy) 91 | env.load({ logger: 'fail' }) 92 | env.postProcess() 93 | }) 94 | 95 | it('should error if config contains reserved keys', function () { 96 | assert.ok(spy.called) 97 | }) 98 | }) 99 | 100 | describe('#config-fail', function () { 101 | var spy = sinon.spy() 102 | 103 | beforeEach(function () { 104 | env.on('error', spy) 105 | env.load({ verbose: true, strict: true }) 106 | env.postProcess() 107 | }) 108 | 109 | it('should not error if config contains verbose or strict keys', function () { 110 | assert.equal(spy.called, false) 111 | }) 112 | }) 113 | 114 | /** 115 | * Default .sassdocrc process. 116 | */ 117 | describe('#sassdocrc', function () { 118 | var sdrc 119 | 120 | var config = { 121 | display: { 122 | access: ['public', 'private'], 123 | alias: true, 124 | watermark: true 125 | } 126 | } 127 | 128 | beforeEach(function () { 129 | sdrc = new mock.SassDocRc(config) 130 | 131 | return sdrc.dump().then(function () { 132 | env.load() 133 | env.postProcess() 134 | }) 135 | }) 136 | 137 | it('should default to .sassdocrc', function () { 138 | assert.ok(warnings.length === 0) 139 | assert.ok(path.basename(env.file) === '.sassdocrc') 140 | assert.deepEqual(env.display, config.display) 141 | }) 142 | 143 | after(function () { 144 | return sdrc.clean() 145 | }) 146 | }) 147 | 148 | /** 149 | * A config.package is passed but fails. 150 | */ 151 | describe('#package-fail', function () { 152 | var spy = sinon.spy() 153 | 154 | beforeEach(function () { 155 | env.on('warning', spy) 156 | env.load({ package: 'should/fail.json' }) 157 | env.postProcess() 158 | }) 159 | 160 | it('should warn if package file is not found and load CWD package.json', function () { 161 | assert.ok(spy.called) 162 | assert.ok(env.package.name === 'sassdoc') 163 | assert.notEqual(-1, warnings[0].indexOf('should/fail.json` not found')) 164 | assert.notEqual(-1, warnings[1].indexOf('Falling back to `package.json`')) 165 | }) 166 | }) 167 | 168 | /** 169 | * Render default theme. 170 | */ 171 | describe('#theme-default', function () { 172 | beforeEach(function () { 173 | env.load() 174 | env.postProcess() 175 | env.data = [] 176 | mkdirp.sync('.sassdoc') 177 | return env.theme('.sassdoc', env) 178 | }) 179 | 180 | it('should render the default theme', function () { 181 | assert.ok(env.themeName === 'default') 182 | assert.ok(fs.existsSync('.sassdoc/index.html')) 183 | assert.ok(fs.existsSync('.sassdoc/assets')) 184 | }) 185 | 186 | after(function (done) { 187 | rimraf('.sassdoc', done) 188 | }) 189 | }) 190 | 191 | /** 192 | * A config.theme is passed but fails. 193 | */ 194 | describe('#theme-fail', function () { 195 | beforeEach(function () { 196 | env.load({ theme: 'fail' }) 197 | env.postProcess() 198 | env.data = [] 199 | mkdirp.sync('.sassdoc') 200 | return env.theme('.sassdoc', env) 201 | }) 202 | 203 | it('should warn and render the default theme', function () { 204 | assert.notEqual(-1, warnings[0].indexOf('Theme `fail` not found')) 205 | assert.notEqual(-1, warnings[1].indexOf('Falling back to default theme')) 206 | assert.ok(env.themeName === 'default') 207 | assert.ok(fs.existsSync('.sassdoc/index.html')) 208 | assert.ok(fs.existsSync('.sassdoc/assets')) 209 | }) 210 | 211 | after(function (done) { 212 | rimraf('.sassdoc', done) 213 | }) 214 | }) 215 | 216 | /** 217 | * A scoped package is passed as theme. 218 | */ 219 | describe('#theme-scoped', function () { 220 | beforeEach(function () { 221 | env.load({ theme: '@test/sassdoc-theme-test' }) 222 | env.postProcess() 223 | env.data = [] 224 | mkdirp.sync('.sassdoc') 225 | return env.theme('.sassdoc', env) 226 | }) 227 | 228 | it('should warn and render the default theme', function () { 229 | assert.notEqual(-1, warnings[0].indexOf('Theme `@test/sassdoc-theme-test` not found')) 230 | assert.notEqual(-1, warnings[1].indexOf('Falling back to default theme')) 231 | assert.ok(env.themeName === 'default') 232 | assert.ok(fs.existsSync('.sassdoc/index.html')) 233 | assert.ok(fs.existsSync('.sassdoc/assets')) 234 | }) 235 | 236 | after(function (done) { 237 | rimraf('.sassdoc', done) 238 | }) 239 | }) 240 | 241 | /** 242 | * ensureEnvironment 243 | */ 244 | describe('#ensureEnvironment', function () { 245 | it('should return a proper Environment instance', function () { 246 | env = ensureEnvironment({ testKey: 'just a test' }) 247 | assert.ok(env instanceof Environment) 248 | assert.ok('testKey' in env) 249 | }) 250 | 251 | it('should call passed callback on error', function () { 252 | var spy = sinon.spy() 253 | env = ensureEnvironment({ logger: Logger.empty }, spy) 254 | env.emit('error', new Error('Triggered from test')) 255 | assert.ok(spy.called) 256 | }) 257 | 258 | it('should trow on error by default', function () { 259 | assert.throws(function () { 260 | env = ensureEnvironment({ logger: Logger.empty }) 261 | env.emit('error', new Error('Triggered from test')) 262 | }) 263 | }) 264 | }) 265 | 266 | /** 267 | * ensureLogger 268 | */ 269 | describe('#ensureLogger', function () { 270 | it('should set a proper Logger instance for env', function () { 271 | env = ensureEnvironment({}) 272 | assert.ok(env.logger instanceof Logger) 273 | }) 274 | 275 | it('should trows if passed logger is not valid', function () { 276 | assert.throws(function () { 277 | ensureEnvironment({ logger: { fail: 'fail' } }) 278 | }) 279 | }) 280 | 281 | it('should let a valid logger pass', function () { 282 | env = ensureEnvironment({ logger: Logger.empty }) 283 | assert.ok(!(env.logger instanceof Logger)) 284 | }) 285 | }) 286 | }) 287 | -------------------------------------------------------------------------------- /test/env/logger.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var assert = require('assert') 4 | var jsesc = require('jsesc') 5 | 6 | var loggerModule = require('../../dist/logger') 7 | var Logger = loggerModule.default 8 | Logger.empty = loggerModule.empty 9 | Logger.checkLogger = loggerModule.checkLogger 10 | 11 | function log (str) { 12 | return '\u001b[32m\xBB\u001b[39m ' + str + '\n' 13 | } 14 | 15 | function warn (str) { 16 | return '\u001b[33m\xBB\u001b[39m [WARNING] ' + str + '\n' 17 | } 18 | 19 | function error (str) { 20 | return '\u001b[31m\xBB\u001b[39m [ERROR] ' + str + '\n' 21 | } 22 | 23 | function debug (str) { 24 | return '\u001b[90m\xBB [DEBUG] ' + str + ' \u001b[39m\n' 25 | } 26 | 27 | var noop = function () {} 28 | 29 | describe('#logger', function () { 30 | var logger 31 | var stderrWrite 32 | var strings = [] 33 | 34 | before(function () { 35 | assert.ok(process.stdout.writable) 36 | assert.ok(process.stderr.writable) 37 | 38 | logger = new Logger(true, true) 39 | 40 | stderrWrite = global.process.stderr.write 41 | 42 | global.process.stderr.write = function (string) { 43 | strings.push(string) 44 | } 45 | 46 | // test logger.log() 47 | logger.log('foo') 48 | logger.log('foo', 'bar') 49 | logger.log('%s %s', 'foo', 'bar', 'hop') 50 | 51 | // test logger.warn() 52 | logger.warn('foo') 53 | logger.warn('foo', 'bar') 54 | logger.warn('%s %s', 'foo', 'bar', 'hop') 55 | 56 | // test logger.error() 57 | logger.error('foo') 58 | logger.error('foo', 'bar') 59 | logger.error('%s %s', 'foo', 'bar', 'hop') 60 | 61 | // test logger.debug() 62 | logger.debug('foo') 63 | logger.debug('foo', 'bar') 64 | logger.debug('%s %s', 'foo', 'bar', 'hop') 65 | logger.debug(function () { return 'foo bar hop hop' }) 66 | 67 | // test logger.timeEnd() 68 | logger.time('label') 69 | logger.timeEnd('label') 70 | logger.time('task') 71 | logger.timeEnd('task', 'Custom %s completed in %dms') 72 | }) 73 | 74 | it('should properly `log` with a green chevron', function () { 75 | assert.equal(log('foo'), strings.shift()) 76 | assert.equal(log('foo bar'), strings.shift()) 77 | assert.equal(log('foo bar hop'), strings.shift()) 78 | }) 79 | 80 | it('should properly `warn` with a yellow chevron', function () { 81 | assert.equal(warn('foo'), strings.shift()) 82 | assert.equal(warn('foo bar'), strings.shift()) 83 | assert.equal(warn('foo bar hop'), strings.shift()) 84 | }) 85 | 86 | it('should properly `error` with a red chevron', function () { 87 | assert.equal(error('foo'), strings.shift()) 88 | assert.equal(error('foo bar'), strings.shift()) 89 | assert.equal(error('foo bar hop'), strings.shift()) 90 | }) 91 | 92 | it('should properly `debug` with a grey chevron', function () { 93 | assert.equal(debug('foo'), strings.shift()) 94 | assert.equal(debug('foo bar'), strings.shift()) 95 | assert.equal(debug('foo bar hop'), strings.shift()) 96 | assert.equal(debug('foo bar hop hop'), strings.shift()) 97 | }) 98 | 99 | it('should properly `timeEnd` with default message', function () { 100 | var re = /\\x1B\[32m\\u2713\\x1B\[39m label: \d+ms\\n/ 101 | var escStr = jsesc(strings.shift()) 102 | assert.ok(re.test(escStr)) 103 | }) 104 | 105 | it('should properly `timeEnd` with custom message', function () { 106 | var re = /\\x1B\[32m\\u2713\\x1B\[39m Custom task completed in \d+ms\\n/ 107 | var escStr = jsesc(strings.shift()) 108 | assert.ok(re.test(escStr)) 109 | }) 110 | 111 | it('should throw if `label` is not defined', function () { 112 | assert.throws(function () { 113 | logger.timeEnd('no such label') 114 | }) 115 | }) 116 | 117 | it('should not throw if `label` is defined', function () { 118 | assert.doesNotThrow(function () { 119 | logger.time('token') 120 | logger.timeEnd('token') 121 | }) 122 | }) 123 | 124 | it('should have a empty logger', function () { 125 | assert.deepEqual(Logger.empty.log(), undefined) 126 | assert.deepEqual(Logger.empty.warn(), undefined) 127 | assert.deepEqual(Logger.empty.error(), undefined) 128 | assert.deepEqual(Logger.empty.debug(), undefined) 129 | }) 130 | 131 | it('should have a function to check for a valid logger', function () { 132 | assert.ok(Logger.checkLogger(Logger.empty)) 133 | assert.throws(function () { 134 | Logger.checkLogger({ log: noop }) 135 | }) 136 | assert.throws(function () { 137 | Logger.checkLogger({ log: noop, warn: noop }) 138 | }) 139 | assert.ok(Logger.checkLogger({ log: noop, warn: noop, error: noop })) 140 | }) 141 | 142 | after(function () { 143 | global.process.stderr.write = stderrWrite 144 | }) 145 | }) 146 | -------------------------------------------------------------------------------- /test/fixture/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "display": { 3 | "access": ["public", "private"], 4 | "alias": false, 5 | "watermark": true 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /test/fixture/denodeify.txt: -------------------------------------------------------------------------------- 1 | whoot 2 | -------------------------------------------------------------------------------- /test/fixture/one.scss: -------------------------------------------------------------------------------- 1 | /// one.scss 2 | -------------------------------------------------------------------------------- /test/fixture/three.scss: -------------------------------------------------------------------------------- 1 | /// three.scss 2 | -------------------------------------------------------------------------------- /test/fixture/two.scss: -------------------------------------------------------------------------------- 1 | /// two.scss 2 | -------------------------------------------------------------------------------- /test/mock.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var path = require('path') 4 | var fs = require('fs') 5 | var inherits = require('util').inherits 6 | var Writable = require('stream').Writable 7 | var Logger = require('../').Logger 8 | var utils = require('../dist/utils') 9 | var writeFile = utils.denodeify(fs.writeFile) 10 | var unlink = utils.denodeify(fs.unlink) 11 | var is = utils.is 12 | 13 | function SassDocRc (config) { 14 | this._filePath = path.join(process.cwd(), '.sassdocrc') 15 | this._contents = config || {} 16 | } 17 | 18 | SassDocRc.prototype.dump = function () { 19 | return writeFile(this._filePath, JSON.stringify(this._contents)) 20 | } 21 | 22 | SassDocRc.prototype.clean = function () { 23 | return unlink(this._filePath) 24 | } 25 | 26 | Object.defineProperty(SassDocRc.prototype, 'contents', { 27 | get: function () { 28 | return this._contents 29 | }, 30 | set: function (config) { 31 | if (!is.plainObject(config)) { 32 | throw new Error('SassDocRc.contents can only be an Object.') 33 | } 34 | this._contents = config 35 | } 36 | }) 37 | 38 | module.exports.SassDocRc = SassDocRc 39 | 40 | function MockLogger () { 41 | Logger.call(this, arguments) 42 | this._output = [] 43 | this._stderr = new Writable() 44 | this._stderr._write = function (chunk, enc, cb) { 45 | this._output.push(chunk.toString()) 46 | cb() 47 | }.bind(this) 48 | } 49 | 50 | inherits(MockLogger, Logger) 51 | 52 | Object.defineProperty(MockLogger.prototype, 'output', { 53 | get: function () { 54 | return this._output 55 | } 56 | }) 57 | 58 | module.exports.Logger = MockLogger 59 | -------------------------------------------------------------------------------- /test/utils/errors.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var assert = require('assert') 4 | var errors = require('../../dist/errors') 5 | 6 | describe('#errors', function () { 7 | var SassDocError 8 | var Warning 9 | var errMess = 'An Error message from test' 10 | var warnMess = 'A Warning message from test' 11 | 12 | before(function () { 13 | SassDocError = new errors.SassDocError(errMess) 14 | Warning = new errors.Warning(warnMess) 15 | }) 16 | 17 | it('should have the proper constructor', function () { 18 | assert.ok(SassDocError instanceof errors.SassDocError) 19 | assert.ok(Warning instanceof errors.Warning) 20 | }) 21 | 22 | it('should have the proper super constructor', function () { 23 | assert.ok(SassDocError instanceof Error) 24 | assert.ok(Warning instanceof errors.SassDocError) 25 | }) 26 | 27 | it('should properly output `name` getter', function () { 28 | assert.ok(SassDocError.name === 'SassDocError') 29 | assert.ok(Warning.name === 'Warning') 30 | }) 31 | 32 | it('should properly output `message` property', function () { 33 | assert.ok(SassDocError.message === errMess) 34 | assert.ok(Warning.message === warnMess) 35 | }) 36 | }) 37 | -------------------------------------------------------------------------------- /test/utils/utils.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var assert = require('assert') 4 | var fs = require('fs') 5 | var utils = require('../../dist/utils') 6 | var readFile = utils.denodeify(fs.readFile) 7 | var errors = require('../../dist/errors') 8 | 9 | describe('#utils:denodeify', function () { 10 | it('should catch errors', function () { 11 | assert.doesNotThrow(function () { 12 | readFile('fail').catch(function (err) { 13 | assert.ok(utils.is.error(err)) 14 | }) 15 | }) 16 | }) 17 | 18 | it('should reject errors', function () { 19 | return readFile('fail') 20 | .catch(function (err) { 21 | assert.ok(utils.is.error(err)) 22 | assert.ok(err.code === 'ENOENT') 23 | }) 24 | }) 25 | 26 | it('should resolve data', function () { 27 | return readFile('test/fixture/denodeify.txt', 'utf8') 28 | .then(function (data) { 29 | assert.ok(utils.is.string(data)) 30 | assert.ok(/whoot/.test(data)) 31 | }) 32 | }) 33 | }) 34 | 35 | describe('#utils:is', function () { 36 | it('should provide utils.is.*', function () { 37 | // .stream 38 | assert.equal(utils.is.stream({ pipe: function () {} }), true) 39 | assert.equal(utils.is.stream(), undefined) 40 | // .undef 41 | assert.equal(utils.is.undef(1), false) 42 | assert.equal(utils.is.undef(), true) 43 | // .error 44 | assert.equal(utils.is.error(null), false) 45 | assert.equal(utils.is.error(new errors.SassDocError()), true) 46 | // .string 47 | assert.equal(utils.is.string(), false) 48 | // .function 49 | assert.equal(utils.is.function(), false) 50 | assert.equal(utils.is.function(function () {}), true) 51 | // .object 52 | assert.equal(utils.is.object(), false) 53 | assert.equal(utils.is.object(1), false) 54 | assert.equal(utils.is.object(''), false) 55 | assert.equal(utils.is.object({}), true) 56 | assert.equal(utils.is.object(new Error()), true) 57 | // .plainObject 58 | assert.equal(utils.is.plainObject(), false) 59 | assert.equal(utils.is.plainObject(1), false) 60 | assert.equal(utils.is.plainObject(new Error()), false) 61 | assert.equal(utils.is.plainObject({}), true) 62 | // .array 63 | assert.equal(utils.is.array(), false) 64 | assert.equal(utils.is.array(1), false) 65 | assert.equal(utils.is.array(''), false) 66 | assert.equal(utils.is.array([]), true) 67 | }) 68 | }) 69 | --------------------------------------------------------------------------------