diff --git a/.gitignore b/.gitignore index 4a2f8a31..fc150d38 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,5 @@ *~ /logs/** /node_modules/** -/public/assets/css/** +/public/assets/css/style.css /public/assets/lib/bower_components/** diff --git a/.hound.yml b/.hound.yml new file mode 100644 index 00000000..1e56eb88 --- /dev/null +++ b/.hound.yml @@ -0,0 +1,8 @@ +fail_on_violations: true +jscs: + enabled: true + config_file: .jscsrc_hound +javascript: + enabled: false +scss: + config_file: .scss.yml diff --git a/.jscsrc b/.jscsrc new file mode 100755 index 00000000..5e650169 --- /dev/null +++ b/.jscsrc @@ -0,0 +1,112 @@ +{ + "additionalRules": [ + "./node_modules/jscs-angular/src/rules/*.js" + ], + "angularAllowDirectiveRestrictions": "EA", + "angularRequireDependencyOrder": "first", + "angularVerifyDependencyUsage": true, + "requireCurlyBraces": [ + "if", + "else", + "for", + "while", + "do", + "try", + "catch" + ], + "requireOperatorBeforeLineBreak": [ + "=", + "+", + "-", + "/", + "*", + "==", + "===", + "!=", + "!==", + ">", + ">=", + "<", + "<=" + ], + "requireCamelCaseOrUpperCaseIdentifiers": {"ignoreProperties": true}, + "maximumLineLength": { + "value": 80, + "allExcept": ["comments", "regex"] + }, + "validateIndentation": 2, + "validateQuoteMarks": "'", + + "disallowMultipleLineStrings": true, + "disallowMixedSpacesAndTabs": true, + "disallowTrailingWhitespace": true, + "disallowSpaceAfterPrefixUnaryOperators": true, + "disallowMultipleVarDecl": { + "allExcept": ["require"] + }, + "disallowKeywordsOnNewLine": ["else"], + + "requireSpaceAfterKeywords": [ + "if", + "else", + "for", + "while", + "do", + "switch", + "return", + "try", + "catch" + ], + "requireSpaceBeforeBinaryOperators": [ + "=", "+=", "-=", "*=", "/=", "%=", "<<=", ">>=", ">>>=", + "&=", "|=", "^=", "+=", + + "+", "-", "*", "/", "%", "<<", ">>", ">>>", "&", + "|", "^", "&&", "||", "===", "==", ">=", + "<=", "<", ">", "!=", "!==" + ], + "requireSpaceAfterBinaryOperators": true, + "requireSpacesInConditionalExpression": true, + "requireSpaceBeforeBlockStatements": true, + "requireSpacesInForStatement": true, + "requireLineFeedAtFileEnd": true, + "requireSpacesInFunctionExpression": { + "beforeOpeningCurlyBrace": true + }, + "disallowSpacesInAnonymousFunctionExpression": { + "beforeOpeningRoundBrace": true + }, + "disallowSpacesInsideObjectBrackets": "all", + "disallowSpacesInsideArrayBrackets": "all", + "disallowSpacesInsideParentheses": true, + + "disallowMultipleLineBreaks": true, + "disallowNewlineBeforeBlockStatements": true, + "disallowKeywords": ["with"], + "disallowSpacesInFunctionExpression": { + "beforeOpeningRoundBrace": true + }, + "disallowSpacesInFunctionDeclaration": { + "beforeOpeningRoundBrace": true + }, + "disallowSpacesInCallExpression": true, + "disallowSpaceAfterObjectKeys": true, + "requireSpaceBeforeObjectValues": true, + "requireCapitalizedConstructors": true, + "requireDotNotation": true, + "requireSemicolons": true, + "validateParameterSeparator": ", ", + + "jsDoc": { + "checkAnnotations": "closurecompiler", + "checkParamNames": true, + "requireParamTypes": true, + "checkRedundantParams": true, + "checkReturnTypes": true, + "checkRedundantReturns": true, + "requireReturnTypes": true, + "checkTypes": true, + "checkRedundantAccess": true, + "requireNewlineAfterDescription": true + } +} \ No newline at end of file diff --git a/.jscsrc_hound b/.jscsrc_hound new file mode 100644 index 00000000..4914305e --- /dev/null +++ b/.jscsrc_hound @@ -0,0 +1,109 @@ +{ + "additionalRules": [ + "./node_modules/jscs-angular/src/rules/*.js" + ], + "requireCurlyBraces": [ + "if", + "else", + "for", + "while", + "do", + "try", + "catch" + ], + "requireOperatorBeforeLineBreak": [ + "=", + "+", + "-", + "/", + "*", + "==", + "===", + "!=", + "!==", + ">", + ">=", + "<", + "<=" + ], + "requireCamelCaseOrUpperCaseIdentifiers": {"ignoreProperties": true}, + "maximumLineLength": { + "value": 80, + "allExcept": ["comments", "regex"] + }, + "validateIndentation": 2, + "validateQuoteMarks": "'", + + "disallowMultipleLineStrings": true, + "disallowMixedSpacesAndTabs": true, + "disallowTrailingWhitespace": true, + "disallowSpaceAfterPrefixUnaryOperators": true, + "disallowMultipleVarDecl": { + "allExcept": ["require"] + }, + "disallowKeywordsOnNewLine": ["else"], + + "requireSpaceAfterKeywords": [ + "if", + "else", + "for", + "while", + "do", + "switch", + "return", + "try", + "catch" + ], + "requireSpaceBeforeBinaryOperators": [ + "=", "+=", "-=", "*=", "/=", "%=", "<<=", ">>=", ">>>=", + "&=", "|=", "^=", "+=", + + "+", "-", "*", "/", "%", "<<", ">>", ">>>", "&", + "|", "^", "&&", "||", "===", "==", ">=", + "<=", "<", ">", "!=", "!==" + ], + "requireSpaceAfterBinaryOperators": true, + "requireSpacesInConditionalExpression": true, + "requireSpaceBeforeBlockStatements": true, + "requireSpacesInForStatement": true, + "requireLineFeedAtFileEnd": true, + "requireSpacesInFunctionExpression": { + "beforeOpeningCurlyBrace": true + }, + "disallowSpacesInAnonymousFunctionExpression": { + "beforeOpeningRoundBrace": true + }, + "disallowSpacesInsideObjectBrackets": "all", + "disallowSpacesInsideArrayBrackets": "all", + "disallowSpacesInsideParentheses": true, + + "disallowMultipleLineBreaks": true, + "disallowNewlineBeforeBlockStatements": true, + "disallowKeywords": ["with"], + "disallowSpacesInFunctionExpression": { + "beforeOpeningRoundBrace": true + }, + "disallowSpacesInFunctionDeclaration": { + "beforeOpeningRoundBrace": true + }, + "disallowSpacesInCallExpression": true, + "disallowSpaceAfterObjectKeys": true, + "requireSpaceBeforeObjectValues": true, + "requireCapitalizedConstructors": true, + "requireDotNotation": true, + "requireSemicolons": true, + "validateParameterSeparator": ", ", + + "jsDoc": { + "checkAnnotations": "closurecompiler", + "checkParamNames": true, + "requireParamTypes": true, + "checkRedundantParams": true, + "checkReturnTypes": true, + "checkRedundantReturns": true, + "requireReturnTypes": true, + "checkTypes": true, + "checkRedundantAccess": true, + "requireNewlineAfterDescription": true + } +} \ No newline at end of file diff --git a/.pullapprove.yml b/.pullapprove.yml new file mode 100644 index 00000000..4acd9208 --- /dev/null +++ b/.pullapprove.yml @@ -0,0 +1,10 @@ +approve_by_comment: true +approve_regex: '\*\*PR approved\*\*|\*\*approv(e|ing)?( this)? PR\*\*|LGTM|:lgtm:|:lgtm_strong:' +reject_regex: '\*\*reject( this)? PR\*\*|cancel( my)? LGTM|:lgtm_cancel:' +reset_on_push: true +author_approval: ignored +reviewers: + required: 1 + members: + - Thatkookooguy + - ortichon diff --git a/.scss.yml b/.scss.yml new file mode 100644 index 00000000..2c57c8d7 --- /dev/null +++ b/.scss.yml @@ -0,0 +1,253 @@ +# Default application configuration that all configurations inherit from. + +scss_files: "**/*.scss" +plugin_directories: ['.scss-linters'] + +# List of gem names to load custom linters from (make sure they are already +# installed) +plugin_gems: [] + +# Default severity of all linters. +severity: warning + +linters: + BangFormat: + enabled: true + space_before_bang: true + space_after_bang: false + + BemDepth: + enabled: false + max_elements: 1 + + BorderZero: + enabled: true + convention: zero # or `none` + + ChainedClasses: + enabled: false + + ColorKeyword: + enabled: true + + ColorVariable: + enabled: true + + Comment: + enabled: true + style: silent + + DebugStatement: + enabled: true + + DeclarationOrder: + enabled: true + + DisableLinterReason: + enabled: false + + DuplicateProperty: + enabled: true + + ElsePlacement: + enabled: true + style: same_line # or 'new_line' + + EmptyLineBetweenBlocks: + enabled: true + ignore_single_line_blocks: true + + EmptyRule: + enabled: true + + ExtendDirective: + enabled: false + + FinalNewline: + enabled: true + present: true + + HexLength: + enabled: false + style: short # or 'long' + + HexNotation: + enabled: true + style: lowercase # or 'uppercase' + + HexValidation: + enabled: true + + IdSelector: + enabled: true + + ImportantRule: + enabled: true + + ImportPath: + enabled: true + leading_underscore: false + filename_extension: false + + Indentation: + enabled: true + allow_non_nested_indentation: false + character: space # or 'tab' + width: 2 + + LeadingZero: + enabled: true + style: include_zero # or 'exclude_zero' + + MergeableSelector: + enabled: true + force_nesting: true + + NameFormat: + enabled: true + allow_leading_underscore: true + convention: hyphenated_lowercase # or 'camel_case', or 'snake_case', or a regex pattern + + NestingDepth: + enabled: false + max_depth: 4 + ignore_parent_selectors: false + + PlaceholderInExtend: + enabled: false + + PrivateNamingConvention: + enabled: false + prefix: _ + + PropertyCount: + enabled: true + include_nested: false + max_properties: 10 + + PropertySortOrder: + enabled: true + ignore_unspecified: false + min_properties: 2 + separate_groups: false + + PropertySpelling: + enabled: true + extra_properties: [] + disabled_properties: [] + + PropertyUnits: + enabled: true + global: [ + 'ch', 'em', 'ex', 'rem', # Font-relative lengths + 'cm', 'in', 'mm', 'pc', 'pt', 'px', 'q', # Absolute lengths + 'vh', 'vw', 'vmin', 'vmax', # Viewport-percentage lengths + 'deg', 'grad', 'rad', 'turn', # Angle + 'ms', 's', # Duration + 'Hz', 'kHz', # Frequency + 'dpi', 'dpcm', 'dppx', # Resolution + '%'] # Other + properties: {} + + PseudoElement: + enabled: true + + QualifyingElement: + enabled: true + allow_element_with_attribute: false + allow_element_with_class: false + allow_element_with_id: false + + SelectorDepth: + enabled: false + max_depth: 2 + + SelectorFormat: + enabled: true + convention: hyphenated_lowercase # or 'strict_BEM', or 'hyphenated_BEM', or 'snake_case', or 'camel_case', or a regex pattern + + Shorthand: + enabled: true + allowed_shorthands: [1, 2, 3] + + SingleLinePerProperty: + enabled: true + allow_single_line_rule_sets: true + + SingleLinePerSelector: + enabled: true + + SpaceAfterComma: + enabled: true + style: one_space # or 'no_space', or 'at_least_one_space' + + SpaceAfterPropertyColon: + enabled: true + style: one_space # or 'no_space', or 'at_least_one_space', or 'aligned' + + SpaceAfterPropertyName: + enabled: true + + SpaceAfterVariableColon: + enabled: false + style: one_space # or 'no_space', 'at_least_one_space' or 'one_space_or_newline' + + SpaceAfterVariableName: + enabled: true + + SpaceAroundOperator: + enabled: true + style: one_space # or 'at_least_one_space', or 'no_space' + + SpaceBeforeBrace: + enabled: true + style: space # or 'new_line' + allow_single_line_padding: false + + SpaceBetweenParens: + enabled: true + spaces: 0 + + StringQuotes: + enabled: true + style: double_quotes # or single_quotes + + TrailingSemicolon: + enabled: true + + TrailingWhitespace: + enabled: true + + TrailingZero: + enabled: false + + TransitionAll: + enabled: false + + UnnecessaryMantissa: + enabled: true + + UnnecessaryParentReference: + enabled: true + + UrlFormat: + enabled: true + + UrlQuotes: + enabled: true + + VariableForProperty: + enabled: false + properties: [] + + VendorPrefix: + enabled: true + identifier_list: base + additional_identifiers: [] + excluded_identifiers: [] + + ZeroUnit: + enabled: true + + Compass::*: + enabled: false diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 00000000..abe9837c --- /dev/null +++ b/.travis.yml @@ -0,0 +1,12 @@ +language: node_js +node_js: +- '0.10' +before_script: +- npm install -g bower +- npm install -g gulp +- npm install -g karma +- bower install +- gulp styles +notifications: + slack: + secure: loJ6a7cTrhv5Sv13BDOVhuvJFuJBTBOI/0rAomL0NWFqJqYvHT2Z8Re07Ltildcb+i5ONMVdU49VCJFiDs2xxHm5RnR12wOw07zFwwawSj9A5ujZyIDvkFa9huYvRMPFJvUi0sJL8vWf6EBF6mxRWC23m5fOU/m6wZHy3dsNbgZbonDTONELJcEnZWKxrxdZPOYcU8Lv7lnnhQ0EGwgefvGGzYOlYICd0q6ZjTHvZor6xEK/3foo/nHxHcv2ttfQe8GsS3GMMlIh7/TLL13W2YXAlypNLundt8rRHPXbDpH60brqDlpPBU3flvsGuIS+by1FP1AEs0c4smchULXgz+fJJzuHWgsljg2IyCN4PmcTaMXFDh2nF6PJAzF2Fy3I5hkrqkn2oYHv3qdSzOlrDuOxVmNYa4CndOeJkzrMQ0ZKiae1Lc9VdCm5fvzLglDfpPq+JbyTHdOJOwy6Z0Cw5qwjl/DuZ7/Uow1xO0t6ufTzKjektfmjde38b5cltEPpGU9zRIe0geB5/jkzjEvJuc2WDB2hzcaeY0d1uNenijGFCENXceQ7YdEH0vWmWTpDWr97+5O4rFK8jNGaFC3+uWy8q/BGTlrTX64R5KFXA2La5L4rJtkx0/xqGopizAeA3qlF5btbcj/xxTYpzpH8Ys+hpmUETJPSgDQYEBESEWY= diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..4fca4b6b --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,93 @@ +# Style Guide + +## Branches + +* Choose *short* and *descriptive* names: + + ```shell + # good + $ git checkout -b oauth-migration + + # bad - too vague + $ git checkout -b login_fix + ``` + +* **Use *dashes* to separate words**, and only lowercase letters. + +* Delete your branch from the upstream repository after it's merged, unless + there is a specific reason not to. + +## Commits + +* Each commit should be a single *logical change*. Don't make several + *logical changes* in one commit. For example, if a patch fixes a bug and + optimizes the performance of a feature, split it into two separate commits. + +* Don't split a single *logical change* into several commits. For example, + the implementation of a feature and the corresponding tests should be in the + same commit. + +* Commit *early* and *often*. Small, self-contained commits are easier to + understand and revert when something goes wrong. + +* Commits should be ordered *logically*. For example, if *commit X* depends + on changes done in *commit Y*, then *commit Y* should come before *commit X*. + +Note: While working alone on a local branch that *has not yet been pushed*, it's +fine to use commits as temporary snapshots of your work. However, it still +holds true that you should apply all of the above *before* pushing it. + +### Messages + +* Use `ungit` or your favorite code editor, not the terminal, when writing a commit message: + + ```shell + # good + $ git commit + + # bad + $ git commit -m "Quick fix" + ``` + + Committing from the terminal encourages a mindset of having to fit everything + in a single line which usually results in non-informative, ambiguous commit + messages. + +* The summary line (ie. the first line of the message) should be + *descriptive* yet *succinct*. Ideally, it should be no longer than + *50 characters*. It should be capitalized and written in imperative present + tense. It should not end with a period since it is effectively the commit + *title*: + + ```shell + # good - imperative present tense, capitalized, fewer than 50 characters + Mark huge records as obsolete when clearing hinting faults + + # bad + fixed ActiveModel::Errors deprecation messages failing when AR was used outside of Rails. + ``` + +# Testing + +We want Kibibit to be used by many people. In order to ensure quality and prevent regressions, all contributions require unit tests proving that the contribution: + +1. Fixes a bug +2. Performs new functionality as expected +3. Behaves in a predictable manner when misused (bad input given as an option for example) + +In addition, where a contribution is aimed at resolving a bug or implementing a feature that can only be measured in a real browser, an e2e test proving the expected behaviour should be included. + +# Link PRs and issues + +To connect between your PR and a specific Issue, write the following somewhere in your PR: `connects to #{PR number}` + +# README + +If your PR adds new behaviour or modifies existing behaviour, the README should be updated. + +# Coding style + +> All code in any code-base should look like a single person typed it, no matter how many people contributed. + +You might not agree with our code styling and that's fine, but if you're going to send PRs, treat this [jscs config file](.jscsrc) as a law. +You can always run jscs to check for any lint errors. just run `gulp lint`. For ***some*** automatic fixes, you can also run `gulp format` diff --git a/ISSUE_TEMPLATE.md b/ISSUE_TEMPLATE.md new file mode 100644 index 00000000..df8c7460 --- /dev/null +++ b/ISSUE_TEMPLATE.md @@ -0,0 +1,10 @@ +### BUG OR FEATURE NAME +###### FEATURE || BUG (keep only relevant sub-header) +--- +*describe your bug \ feature* + +*try and add details as needed. think that someone else needs to understand what you want without being inside your head* + +*please add relevant labels & the correct milestone* + +*add the `help wanted` label if you want to open a discussion* diff --git a/PULL_REQUEST_TEMPLATE.md b/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 00000000..a1ff25f1 --- /dev/null +++ b/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,26 @@ +# Change Summary + + - change short synopsis I [resolves #00] + + > additional info about this mission + + - change short synopsis II (mission without additional info) + - change short synopsis III + +## More info + +(Free text here with additional photos, wikis, and additional needed information) + +# :warning: MAKE SURE TO GO THROUGH THIS LIST :warning: +(reviewers ***shouldn't approve a PR without the next three checkboxs checked***) + +Before you submit a PR, make sure you did the following things: +- [ ] did you link this PR to an issue? + +>> Make sure there's an issue open about the change you did *(open one if there isn't)*. link this PR to that issue by writing `resolves #{{issue_number}}`. If this PR had several mission, link each issue in its parallel mission. + +- [ ] did you lint your changes to both javascript and scss? + +>> hound can be **pretty rough** at keeping our code clean and readable! make sure you format your code by running either `gulp format` or `gulp lint` and cleaning out all the ***lint errors***. If you won't, you'll have a ***long conversation*** with hound :-) + +- [ ] "I'm pretty sure I'll be able to read and understand this PR, even if I wasn't the author." - ***said the PR author*** diff --git a/Procfile b/Procfile new file mode 100644 index 00000000..489b2700 --- /dev/null +++ b/Procfile @@ -0,0 +1 @@ +web: node server.js diff --git a/README.md b/README.md new file mode 100644 index 00000000..c9c36a67 --- /dev/null +++ b/README.md @@ -0,0 +1,17 @@ +#  code editor [](http://github.com/Kibibit/kibibit-code-editor) [](http://github.com/Kibibit/kibibit-code-editor/fork) [](https://travis-ci.org/Kibibit/kibibit-code-editor) [](https://gemnasium.com/Kibibit/kibibit-code-editor) [](https://kibibit-demo.herokuapp.com/) + +Hi there! + +Code on your own server from afar. This tool is meant to be used by individuals and teams. + +Currently, we're focusing on web related programming (but requests are welcome! Just open an issue) + + + +## Collaboration + +Your help is appreciated! If you've found a bug or if something is not clear, please raise an issue. + +Ideally, if you've found an issue, you can submit a PR that meets our [contributor guidelines](CONTRIBUTING.md). + +[](https://waffle.io/Kibibit/kibibit-code-editor/metrics) diff --git a/app.json b/app.json new file mode 100644 index 00000000..c78d4ac3 --- /dev/null +++ b/app.json @@ -0,0 +1,12 @@ +{ + "name": "kibibit-code-editor", + "description": "Remote code editor to your face!", + "repository": "https://github.com/Kibibit/kibibit-code-editor", + "env": { + "NODE_ENV": "development", + "NPM_CONFIG_PRODUCTION": "false" + }, + "addons": [ + + ] +} diff --git a/app/models/fileService.js b/app/models/fileService.js index e1b3ef65..64efc390 100644 --- a/app/models/fileService.js +++ b/app/models/fileService.js @@ -1,91 +1,130 @@ -var console = process.console, - dirTree = require('directory-tree'), +var dirTree = require('directory-tree'), fs = require('fs'), - path = require("path"); + mime = require('mime-types'); +var console = process.console; var fileService = {}; fileService.get = function(req, res) { - var fileFullPath = req.params.file_id; + var fileFullPath = req.params.file_id; + var mimeType = mime.lookup(fileFullPath) || ''; + + // fix case for ES6 files (currently, highlight them as regular javascript) + if (!mimeType && fileFullPath.toLowerCase().endsWith('es6')) { + mimeType = 'application/javascript'; + } + + var isFileOfType = function(type) { + return mimeType.indexOf(type) !== -1; + }; + + var showNoContent = false || + isFileOfType('zip') || + isFileOfType('program') || + isFileOfType('image') || + isFileOfType('font'); + + // temprorary solution until we have a view selector on the FRONT-END + if (showNoContent) { + res.json({ + content: 'awww man... we can\'t show ' + mimeType + ' yet :-(', + mimeType: 'text/text' + }); + } else { fs.readFile(fileFullPath, 'utf8', function(err, data) { - if (err) { - res.json(err); - console.time().tag('FILE CONTENT').error('file-get returned an error: ' + err); - } else { - res.json(data); - console.time().tag('FILE CONTENT').info('file requested: ' + fileFullPath); - } + if (err) { + res.json(err); + console.time().tag('FILE CONTENT') + .error('file-get returned an error: ' + err); + } else { + var file = { + content: data, + mimeType: mimeType + }; + res.json(file); + console.time().tag('FILE CONTENT') + .info('file requested: ' + fileFullPath); + } }); + } }; fileService.put = function(req, res) { - var fileFullPath = req.params.file_id; - try { - var stat = fs.statSync(fileFullPath); - res.json({ - "errno": "330", - "code": "File already exists", - "path": fileFullPath - }); - console.time().tag('FILE CONTENT').error('file couldn\'t be saved: ' + fileFullPath); - } catch (err) { - if (req.body.newContent) { - fs.writeFile(fileFullPath, req.body.newContent, 'utf8', function(err) { - if (err) { - res.json(err); - console.time().tag('FILE CONTENT').error('file couldn\'t be saved: ' + err); - } else { - res.json({ - message: 'file saved successfully' - }); - console.time().tag('FILE CONTENT').info('file saved: ' + fileFullPath); - } - }); + var fileFullPath = req.params.file_id; + try { + var stat = fs.statSync(fileFullPath); + res.json({ + 'errno': '330', + 'code': 'File already exists', + 'path': fileFullPath + }); + console.time().tag('FILE CONTENT') + .error('file couldn\'t be saved: ' + fileFullPath); + } catch (err) { + if (req.body.newContent) { + fs.writeFile(fileFullPath, + req.body.newContent, 'utf8', function(err) { + if (err) { + res.json(err); + console.time().tag('FILE CONTENT') + .error('file couldn\'t be saved: ' + err); + } else { + res.json({ + message: 'file saved successfully' + }); + console.time().tag('FILE CONTENT') + .info('file saved: ' + fileFullPath); } + }); } + } }; fileService.delete = function(req, res) { - var fileFullPath = req.params.file_id; - try { - // rm fiilename - fs.unlinkSync(fileFullPath); - res.json({ - message: 'file deleted successfully' - }); - console.time().tag('DELETE CONTENT').info('file deleted: ' + fileFullPath); - } catch (err) { - res.json(err); - console.time().tag('DELETE CONTENT').error('couldn\'t delete file: ' + fileFullPath); - } + var fileFullPath = req.params.file_id; + try { + // rm fiilename + fs.unlinkSync(fileFullPath); + res.json({ + message: 'file deleted successfully' + }); + console.time().tag('DELETE CONTENT') + .info('file deleted: ' + fileFullPath); + } catch (err) { + res.json(err); + console.time().tag('DELETE CONTENT') + .error('couldn\'t delete file: ' + fileFullPath); + } }; fileService.putExtraArg = function(req, res) { - var fileFullPath = req.params.file_id; - var isHardSave = req.params.extra_arg; - if (isHardSave === "true") { - if (req.body.newContent || 0 === req.body.newContent.length) { - fs.writeFile(fileFullPath, req.body.newContent, 'utf8', function(err) { - if (err) { - res.json(err); - console.time().tag('FILE CONTENT').error('file couldn\'t be saved: ' + err); - } else { - res.json({ - message: 'file saved successfully' - }); - console.time().tag('FILE CONTENT').info('file saved: ' + fileFullPath); - } - }); + var fileFullPath = req.params.file_id; + var isHardSave = req.params.extra_arg; + if (isHardSave === 'true') { + if (req.body.newContent || 0 === req.body.newContent.length) { + fs.writeFile(fileFullPath, req.body.newContent, 'utf8', function(err) { + if (err) { + res.json(err); + console.time().tag('FILE CONTENT') + .error('file couldn\'t be saved: ' + err); } else { - res.json({ - message: 'you have to include a body with newContent' - }); + res.json({ + message: 'file saved successfully' + }); + console.time().tag('FILE CONTENT') + .info('file saved: ' + fileFullPath); } + }); } else { - res.json({ - message: 'For a hard save, 2nd param should be: True' - }); + res.json({ + message: 'you have to include a body with newContent' + }); } + } else { + res.json({ + message: 'For a hard save, 2nd param should be: True' + }); + } }; diff --git a/app/models/folderService.js b/app/models/folderService.js index 7fd1e89d..6cc6517f 100644 --- a/app/models/folderService.js +++ b/app/models/folderService.js @@ -1,132 +1,171 @@ -var console = process.console, - dirTree = require('directory-tree'), +var dirTree = require('directory-tree'), fs = require('fs'), - path = require("path"); + path = require('path'); +var console = process.console; var folderService = {}; folderService.get = function(req, res) { - var output = {}; - var directoryFullPath = req.params.dir_id; - fs.readdir(directoryFullPath, function(err, items) { - try { - output.path = directoryFullPath; - output.name = 'Development'; - output.type = 'directory'; - output.children = []; + var output = {}; + var directoryFullPath = req.params.dir_id; + fs.readdir(directoryFullPath, function(err, items) { + try { + output.path = directoryFullPath; + output.name = directoryFullPath.split(/\/|\\/).reverse()[0]; + output.type = 'directory'; + output.children = []; - for (var i = 0; i < items.length; i++) { - if (fs.lstatSync(directoryFullPath + '/' + items[i]).isDirectory()) { - output.children.push({ - name: items[i], - path: directoryFullPath + '/' + items[i], - type: 'directory', - children: [] - }); - } else { - output.children.push({ - name: items[i], - path: directoryFullPath + '/' + items[i], - type: folderService.getFileExtension(items[i]) - }); - } - } + for (var i = 0; i < items.length; i++) { + if (fs.lstatSync(directoryFullPath + + '/' + items[i]).isDirectory()) { + output.children.push({ + name: items[i], + path: directoryFullPath + '/' + items[i], + type: 'directory', + children: [] + }); + } else { + output.children.push({ + name: items[i], + path: directoryFullPath + '/' + items[i], + type: folderService.getFileExtension(items[i]) + }); + } + } + output.children.sort(compare); + res.json(output); + } catch (error) { + res.json(error); + console.time().tag('DIRECTORY CONTENT') + .error('directory requested not found: ' + directoryFullPath); + } + }); - res.json(output); - } catch (err) { - res.json(err); - console.time().tag('DIRECTORY CONTENT').error('directory requested not found: ' + directoryFullPath); + function compare(a, b) { + if (a.type === 'directory') { + if (b.type === 'directory') { + if (a.name < b.name) { + return -1; + } else if (a.name > b.name) { + return 1; + } else { + return 0; } - }); + } else { + return -1; + } + } else { + if (b.type === 'directory') { + return 1; + } else { + if (a.name < b.name) { + return -1; + } else if (a.name > b.name) { + return 1; + } else { + return 0; + } + } + } + } }; folderService.getFileExtension = function(filename) { - var a = filename.split("."); - if (a.length === 1 || (a[0] === "" && a.length === 2)) { - return ""; - } - return a.pop().toLowerCase(); + var a = filename.split('.'); + if (a.length === 1 || (a[0] === '' && a.length === 2)) { + return ''; + } + return a.pop().toLowerCase(); }; folderService.put = function(req, res) { - var directoryFullPath = req.params.dir_id; - fs.mkdir(directoryFullPath, function(err) { - if (err) { - res.json(err); - } else { - res.json({ - message: 'Folder created successfully' - }); - console.time().tag('DIRECTORY CONTENT').info('directory created: ' + directoryFullPath); - } - }); + var directoryFullPath = req.params.dir_id; + fs.mkdir(directoryFullPath, function(err) { + if (err) { + res.json(err); + } else { + res.json({ + message: 'Folder created successfully' + }); + console.time().tag('DIRECTORY CONTENT') + .info('directory created: ' + directoryFullPath); + } + }); }; folderService.delete = function(req, res) { - var directoryFullPath = req.params.dir_id; - fs.rmdir(directoryFullPath, function(err) { - if (err && err.code == "ENOTEMPTY") { - // couldn't delete file - console.error(err); - } - }); - res.json(directoryFullPath); - console.time().tag('DIRECTORY CONTENT').info('directory deleted: ' + directoryFullPath); + var directoryFullPath = req.params.dir_id; + fs.rmdir(directoryFullPath, function(err) { + if (err && err.code == 'ENOTEMPTY') { + // couldn't delete file + console.error(err); + } + }); + res.json(directoryFullPath); + console.time().tag('DIRECTORY CONTENT') + .info('directory deleted: ' + directoryFullPath); }; folderService.putExtraArg = function(req, res) { - var directoryFullPath = req.params.dir_id; - var newDirectoryFullPath = req.params.extra_arg; - fs.rename(directoryFullPath, newDirectoryFullPath, function(err) { - if (err) { - res.json(err); - } else { - console.time().tag('DIRECTORY RENAME').info('directory renamed from: ' + directoryFullPath + '; to: ' + newDirectoryFullPath); - res.json({ - message: 'Folder renamed successfully' - }); - } - }); + var directoryFullPath = req.params.dir_id; + var newDirectoryFullPath = req.params.extra_arg; + fs.rename(directoryFullPath, newDirectoryFullPath, function(err) { + if (err) { + res.json(err); + } else { + console.time().tag('DIRECTORY RENAME') + .info('directory renamed from: ' + + directoryFullPath + + '; to: ' + + newDirectoryFullPath); + res.json({ + message: 'Folder renamed successfully' + }); + } + }); }; folderService.deleteExtraArg = function(req, res) { - var directoryFullPath = req.params.dir_id; - var isHardDelete = req.params.extra_arg; - var rmdir = function(dir) { - var list = fs.readdirSync(dir); - for (var i = 0; i < list.length; i++) { - var filename = path.join(dir, list[i]); - var stat = fs.statSync(filename); + var directoryFullPath = req.params.dir_id; + var isHardDelete = req.params.extra_arg; + var rmdir = function(dir) { + var list = fs.readdirSync(dir); + for (var i = 0; i < list.length; i++) { + var filename = path.join(dir, list[i]); + var stat = fs.statSync(filename); - if (filename == "." || filename == "..") { - // pass these files - } else if (stat.isDirectory()) { - // rmdir recursively - rmdir(filename); - } else { - // rm fiilename - fs.unlinkSync(filename); - } - } - fs.rmdirSync(dir); - }; + if (filename == '.' || filename == '..') { + // pass these files + } else if (stat.isDirectory()) { + // rmdir recursively + rmdir(filename); + } else { + // rm fiilename + fs.unlinkSync(filename); + } + } + fs.rmdirSync(dir); + }; - try { - if (isHardDelete === "true") { - rmdir(directoryFullPath); - res.json({ - message: 'Directory deleted (recursively) successfully' - }); - console.time().tag('DIRECTORY DELETED').info('directory deleted (recursively) successfully: ' + directoryFullPath); - } else { - res.json({ - message: 'For a hard delete, 2nd param should be: True' - }); - } - } catch (err) { - res.json(err); - console.time().tag('DIRECTORY DELETED').error('directory wasn\'t found: ' + directoryFullPath); + try { + if (isHardDelete === 'true') { + rmdir(directoryFullPath); + res.json({ + message: 'Directory deleted (recursively) successfully' + }); + console.time().tag('DIRECTORY DELETED') + .info('directory deleted (recursively) successfully: ' + + directoryFullPath); + } else { + res.json({ + message: 'For a hard delete, 2nd param should be: True' + }); } + } catch (err) { + res.json(err); + console.time().tag('DIRECTORY DELETED') + .error('directory wasn\'t found: ' + directoryFullPath); + } }; diff --git a/app/models/user.js b/app/models/user.js index cd83c107..6f636d2e 100644 --- a/app/models/user.js +++ b/app/models/user.js @@ -2,45 +2,47 @@ var mongoose = require('mongoose'); var Schema = mongoose.Schema; var bcrypt = require('bcrypt-nodejs'); -// user schema +// user schema var UserSchema = new Schema({ - name: String, - username: { - type: String, - required: true, - index: { - unique: true - } - }, - password: { - type: String, - required: true, - select: false + name: String, + username: { + type: String, + required: true, + index: { + unique: true } + }, + password: { + type: String, + required: true, + select: false + } }); // hash the password before the user is saved UserSchema.pre('save', function(next) { - var user = this; + var user = this; - // hash the password only if the password has been changed or user is new - if (!user.isModified('password')) return next(); + // hash the password only if the password has been changed or user is new + if (!user.isModified('password')) { + return next(); + } - // generate the hash - bcrypt.hash(user.password, null, null, function(err, hash) { - if (err) return next(err); + // generate the hash + bcrypt.hash(user.password, null, null, function(err, hash) { + if (err) { return next(err); } - // change the password to the hashed version - user.password = hash; - next(); - }); + // change the password to the hashed version + user.password = hash; + next(); + }); }); // method to compare a given password with the database hash UserSchema.methods.comparePassword = function(password) { - var user = this; + var user = this; - return bcrypt.compareSync(password, user.password); + return bcrypt.compareSync(password, user.password); }; module.exports = mongoose.model('User', UserSchema); diff --git a/app/models/userHomeDirectoryService.js b/app/models/userHomeDirectoryService.js index b6380e59..a79782a9 100644 --- a/app/models/userHomeDirectoryService.js +++ b/app/models/userHomeDirectoryService.js @@ -1,11 +1,12 @@ -var console = process.console, - userHomeDirectory = require('user-home'); +var userHomeDirectory = require('user-home'); +var console = process.console; var userHomeDirectoryService = {}; userHomeDirectoryService.get = function(req, res) { - res.json(userHomeDirectory); - console.time().tag('DIRECTORY CONTENT').info('user home directory sent : ' + userHomeDirectory); + res.json(userHomeDirectory); + console.time().tag('DIRECTORY CONTENT') + .info('user home directory sent : ' + userHomeDirectory); }; module.exports = userHomeDirectoryService; diff --git a/app/routes/api.js b/app/routes/api.js index 36204452..96d7bb58 100644 --- a/app/routes/api.js +++ b/app/routes/api.js @@ -1,49 +1,47 @@ module.exports = function(app, express) { - var apiRouter = express.Router(), - fileService = require('../models/fileService.js'), - folderService = require('../models/folderService.js'), - userHomeDirectoryService = require('../models/userHomeDirectoryService.js'); - - apiRouter.get('/', function(req, res) { - res.json({ - message: 'hooray! welcome to our api!' - }); - }); - - apiRouter.route('/file/:file_id') - // get the file content with that file_id = full file path - .get(fileService.get) - // update the file content with this id - .put(fileService.put) - // delete the file content with this id - .delete(fileService.delete); - - - apiRouter.route('/file/:file_id/:extra_arg') - // delete the file content with this id - .put(fileService.putExtraArg); - + var fileService = require('../models/fileService.js'), + folderService = require('../models/folderService.js'), + userHomeDirectoryService = + require('../models/userHomeDirectoryService.js'); - apiRouter.route('/directory/:dir_id') - // get the directory content with that directory_id = full file path - .get(folderService.get) - // create a new directory with the dir_id - .put(folderService.put) - // delete the directory content with this id - .delete(folderService.delete); + var apiRouter = express.Router(); - - apiRouter.route('/directory/:dir_id/:extra_arg') - // ---------------------------------------------------- - // rename directory - .put(folderService.putExtraArg) - // hard delete the directory content with this id. This means all the files inside are deleted recursively - .delete(folderService.deleteExtraArg); - - - apiRouter.route('/userHomeDirectory/') - .get(userHomeDirectoryService.get) - - return apiRouter; + apiRouter.get('/', function(req, res) { + res.json({ + message: 'hooray! welcome to our api!' + }); + }); + + apiRouter.route('/file/:file_id') + // get the file content with that file_id = full file path + .get(fileService.get) + // update the file content with this id + .put(fileService.put) + // delete the file content with this id + .delete(fileService.delete); + + apiRouter.route('/file/:file_id/:extra_arg') + // delete the file content with this id + .put(fileService.putExtraArg); + + apiRouter.route('/directory/:dir_id') + // get the directory content with that directory_id = full file path + .get(folderService.get) + // create a new directory with the dir_id + .put(folderService.put) + // delete the directory content with this id + .delete(folderService.delete); + + apiRouter.route('/directory/:dir_id/:extra_arg') + // ---------------------------------------------------- + // rename directory + .put(folderService.putExtraArg) + // hard delete the directory content with this id. This means all the files inside are deleted recursively + .delete(folderService.deleteExtraArg); + + apiRouter.route('/userHomeDirectory/') + .get(userHomeDirectoryService.get); + + return apiRouter; }; diff --git a/bower.json b/bower.json index c54e3f6a..3024bbba 100644 --- a/bower.json +++ b/bower.json @@ -2,6 +2,27 @@ "name": "kibibit-code-editor", "ignore": [], "dependencies": { - "angular-resizable": "~1.2.0" + "angular-resizable": "~1.2.0", + "angular": "~1.4.8", + "angular-animate": "~1.4.8", + "angular-aria": "~1.4.8", + "angular-material": "~1.0.3", + "angular-route": "~1.4.8", + "angular-loading-bar": "~0.8.0", + "octicons": "~3.4.1", + "ng-dialog": "~0.5.6", + "angular-fullscreen": "~1.0.1", + "angular-ui-layout": "~1.4.2", + "ng-scrollbars": "~0.0.7", + "angular-marked": "~1.0.1", + "angular-emoji": "angular-emoji-filter#~0.0.2", + "angular-sanitize": "~1.5.0", + "json-formatter": "~0.5.0", + "velocity": "~1.2.3", + "ngclipboard": "~1.1.1", + "ng-device-detector": "~3.0.1" + }, + "resolutions": { + "angular": "1.5.6" } } diff --git a/config.js b/config.js index 84f241f6..8c2f5dd5 100644 --- a/config.js +++ b/config.js @@ -1,5 +1,5 @@ module.exports = { - 'port': process.env.PORT || 3141, - 'database': 'mongodb://node:noder@novus.modulusmongo.net:27017/Iganiq8o', - 'secret': 'ilovescotchscotchyscotchscotch' + 'port': process.env.PORT || 3141, + 'database': 'mongodb://node:noder@novus.modulusmongo.net:27017/Iganiq8o', + 'secret': 'ilovescotchscotchyscotchscotch' }; diff --git a/gulpfile.js b/gulpfile.js index eba66dfd..f7482a20 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -1,120 +1,162 @@ // grab our packages -var gulp = require('gulp'), +var gulp = require('gulp-help')(require('gulp'), { + description: 'you are looking at it.', + aliases: ['h'] +}), +colors = require('colors'), beautify = require('gulp-jsbeautifier'), sourcemaps = require('gulp-sourcemaps'), sass = require('gulp-sass'), -minifyCSS = require('gulp-minify-css'), +cssnano = require('gulp-cssnano'), rename = require('gulp-rename'), prettify = require('gulp-jsbeautifier'), -jshint = require('gulp-jshint'); +concat = require('gulp-concat'), +livereload = require('gulp-livereload'), +server = require('gulp-develop-server'), +shell = require('gulp-shell'), +jscs = require('gulp-jscs'), +argv = require('yargs').argv, +cache = require('gulp-cached'), +gutil = require('gulp-util'), +depcheck = require('gulp-depcheck'); + +var karma = require('karma').server; + +var isTravis = process.env.TRAVIS || false; + +var indent = ' '; +var options = { + path: './server.js' +}; +var FILES = {}; +FILES.FRONTEND_JS = ['./public/app/**/*.js']; +FILES.FRONTEND_HTML = ['./public/app/**/*.html']; +FILES.FRONTEND_SASS = ['./public/assets/sass/**/*.scss']; +FILES.FRONTEND_ALL = [].concat(FILES.FRONTEND_JS, FILES.FRONTEND_HTML, FILES.FRONTEND_SASS); +FILES.SERVER_MAIN = ['./server.js']; +FILES.SERVER_JS_WITHOUT_MAIN = ['./app/**/*.js', './config.js']; +FILES.SERVER_JS = [].concat(FILES.SERVER_MAIN, FILES.SERVER_JS_WITHOUT_MAIN); +FILES.BUILD_FILES = ['./gulpfile.js']; +FILES.JS_ALL = [].concat(FILES.FRONTEND_JS, FILES.SERVER_JS); +FILES.LINT = [].concat(FILES.FRONTEND_JS, FILES.SERVER_JS_WITHOUT_MAIN); // define the default task and add the watch task to it -gulp.task('default', ['watch']); +gulp.task('default', colors.bgCyan.black('gulp') + ' === ' + colors.bgCyan.black('gulp watch'), ['watch']); + +gulp.task('test', 'run all tests using karma locally, and travis-ci on GitHub', + function(done) { + console.log('isTravis', isTravis); + karma.start({ + configFile: __dirname + '/karma.conf.js', + singleRun: isTravis + }, done); + } +); + +gulp.task('depcheck', + 'checks for unused dependencies ' + colors.blue('(including devs)'), + depcheck({ + ignoreDirs: ['test', 'logs'], + ignoreMatches: ['karma-*', 'jscs-*', 'jasmine-*'] + }) +); // configure the jshint task -gulp.task('lint-js', function() { - return gulp.src(['./**/*.js', '!./node_modules/', '!./node_modules/**', '!./logs/']) - .pipe(jshint()) - .pipe(jshint.reporter('jshint-stylish')); -}); +gulp.task('lint-js', 'lint ' + colors.blue('all JS') + ' files in the following paths:\n' + indent + + colors.yellow(FILES.JS_ALL.join(',\n' + indent)), + function() { + return gulp.src(FILES.JS_ALL) + .pipe(cache('linting')) + .pipe(jscs()) + .pipe(jscs.reporter()) + .pipe(jscs.reporter('fail')); + }); -gulp.task('lint-sass', function() { - return gulp.src('./public/assets/sass/**/*.scss') - .pipe(sass().on('error', sass.logError)); -}); +gulp.task('lint-sass', 'lint ' + colors.blue('all SASS') + ' files in the following paths:\n' + indent + + colors.yellow(FILES.FRONTEND_SASS.join(',\n' + indent)), + function() { + return gulp.src(FILES.FRONTEND_SASS) + .pipe(cache('linting')) + .pipe(sass().on('error', sass.logError)); + }); -gulp.task('lint', ['lint-js', 'lint-sass']); - -gulp.task('format-front-end', function() { - return gulp.src([ - /* JavaScript */ - './public/app/**/*.js', // front-end javascript - /* HTML */ - './public/app/**/*.html', // front-end javascript - ], { - base: 'public' - }) - .pipe(prettify({ - //config: "./.jsbeautifyrc", - html: { - braceStyle: "collapse", - indentChar: " ", - indentScripts: "keep", - indentSize: 4, - maxPreserveNewlines: 10, - preserveNewlines: true, - /* unformatted: ["a", "sub", "sup", "b", "i", "u"], */ - wrapLineLength: 0 - }, - js: { - braceStyle: "collapse", - breakChainedMethods: false, - e4x: false, - evalCode: false, - indentChar: " ", - indentLevel: 0, - indentSize: 4, - indentWithTabs: false, - jslintHappy: false, - keepArrayIndentation: false, - keepFunctionIndentation: false, - maxPreserveNewlines: 10, - preserveNewlines: true, - spaceBeforeConditional: true, - spaceInParen: false, - unescapeStrings: false, - wrapLineLength: 0 - } - })) - .pipe(gulp.dest('./public')); // add this to a different folder in order to test first -}); +gulp.task('lint', 'lint ' + colors.blue('all javascript and sass') + ' files', ['lint-js', 'lint-sass']); -gulp.task('format-server', function() { - return gulp.src([ - /* JavaScript */ - './app/**/*.js', - './server.js' - ], { - base: '.' - }) - .pipe(prettify({ - js: { - braceStyle: "collapse", - breakChainedMethods: false, - e4x: false, - evalCode: false, - indentChar: " ", - indentLevel: 0, - // indentSize: 4, - indentWithTabs: false, - jslintHappy: false, - keepArrayIndentation: false, - keepFunctionIndentation: false, - maxPreserveNewlines: 10, - preserveNewlines: true, - spaceBeforeConditional: true, - spaceInParen: false, - unescapeStrings: false, - wrapLineLength: 0 - } - })) - .pipe(gulp.dest('.')); -}); +gulp.task('format-front-end', 'formats the FE files in the following paths:\n' + indent + + colors.yellow(FILES.FRONTEND_JS.join(',\n' + indent)), + function() { + return gulp.src([].concat(FILES.FRONTEND_JS), { + base: 'public' + }) + .pipe(cache('formating')) + .pipe(jscs({ + fix: true + })) + .pipe(jscs.reporter()) + .pipe(gulp.dest('./public')); // add this to a different folder in order to test first + }); + +gulp.task('format-server', 'formats the BE files in the following paths:\n' + indent + + colors.yellow([].concat(FILES.SERVER_JS, FILES.BUILD_FILES).join(',\n' + indent)), + function() { + return gulp.src([].concat(FILES.SERVER_JS, FILES.BUILD_FILES), { + base: '.' + }) + .pipe(cache('formating')) + .pipe(jscs({ + fix: true + })) + .pipe(jscs.reporter()) + .pipe(gulp.dest('.')); + }); -gulp.task('format', ['format-server', 'format-front-end']); +gulp.task('format', 'formats ' + colors.blue('all') + ' the project\'s javascript files', ['format-server', 'format-front-end']); -gulp.task('styles', function() { - return gulp.src('./public/assets/sass/**/*.scss') - .pipe(sourcemaps.init()) - .pipe(sass().on('error', sass.logError)) - //.pipe(minifyCSS()) - .pipe(sourcemaps.write()) - //.pipe(rename({ suffix: '.min' })) - .pipe(gulp.dest('./public/assets/css/')); +gulp.task('styles', 'compile SASS to CSS', function() { + return gulp.src(FILES.FRONTEND_SASS) + .pipe(sourcemaps.init()) + .pipe(concat('style.css')) + .pipe(sass().on('error', sass.logError)) + //.pipe(cssnano()) + .pipe(sourcemaps.write()) + //.pipe(rename({ suffix: '.min' })) + .pipe(gulp.dest('./public/assets/css/')); }); -// configure which files to watch and what tasks to use on file changes -gulp.task('watch', function() { - gulp.watch(['./**/*.js', '!./node_modules/', '!./node_modules/**', '!./logs/'], ['lint-js']); - gulp.watch('./public/assets/sass/**/*.scss', ['styles']); +gulp.task('serve', 'start the Kibibit Code Editor server', ['styles'], function() { + server.listen(options, livereload.listen); }); + +gulp.task('debug', 'debug the project using ' + colors.blue('~= node-inspector =~'), ['styles'], shell.task(['node-debug server.js'])); + +// configure which files to watch and what tasks to use on file changes +gulp.task('watch', 'first, will compile SASS and run the server.\n' + indent + + 'Then, it watches changes and do the following things when needed:\n' + indent + + colors.yellow(' 1.') + ' compile SASS\n' + indent + + colors.yellow(' 2.') + ' restart server\n' + indent + + colors.yellow(' 3.') + ' reload browser', ['serve'], + function() { + function restart(file) { + server.changed(function(error) { + if (!error) { + reloadBrowser('Backend file changed.', file.path); + } + }); + } + + function reloadBrowser(message, path) { + gutil.log(message ? message : 'Something changed.', gutil.colors.bgBlue.white.bold('Reloading browser...')); + livereload.changed(path); + } + + gulp.watch(argv.lint ? FILES.LINT : [], ['lint-js']); + gulp.watch(FILES.FRONTEND_SASS, ['styles']); + gulp.watch(FILES.SERVER_JS).on('change', restart); + gulp.watch(FILES.FRONTEND_ALL).on('change', function(file) { + reloadBrowser('Frontend file changed.', file.path); + }); + }, { + options: { + 'lint': 'will include output from linter only for changed files' + } + }); diff --git a/karma.conf.js b/karma.conf.js new file mode 100644 index 00000000..2139f082 --- /dev/null +++ b/karma.conf.js @@ -0,0 +1,69 @@ +// Karma configuration +// Generated on Sat Jan 02 2016 18:50:53 GMT+0200 (IST) + +module.exports = function(config) { + config.set({ + + // base path that will be used to resolve all patterns (eg. files, exclude) + basePath: '', + + + // frameworks to use + // available frameworks: https://npmjs.org/browse/keyword/karma-adapter + frameworks: ['jasmine'], + + + // list of files / patterns to load in the browser + files: [ + 'test/hello.js' + ], + + + // list of files to exclude + exclude: [ + ], + + + // preprocess matching files before serving them to the browser + // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor + preprocessors: { + }, + + + // test results reporter to use + // possible values: 'dots', 'progress' + // available reporters: https://npmjs.org/browse/keyword/karma-reporter + reporters: ['progress'], + + + // web server port + port: 9876, + + + // enable / disable colors in the output (reporters and logs) + colors: true, + + + // level of logging + // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG + logLevel: config.LOG_INFO, + + + // enable / disable watching file and executing tests whenever any file changes + autoWatch: true, + + + // start these browsers + // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher + browsers: ['PhantomJS'], + + + // Continuous Integration mode + // if true, Karma captures browsers, runs the tests and exits + singleRun: false, + + // Concurrency level + // how many browser should be started simultaneous + concurrency: Infinity + }) +} diff --git a/package.json b/package.json index 9759d20c..32d9505a 100644 --- a/package.json +++ b/package.json @@ -4,34 +4,55 @@ "description": "code editor on the cloud", "main": "server.js", "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" + "postinstall": "node ./node_modules/bower/lib/bin/bower install && node ./node_modules/gulp/bin/gulp styles", + "start": "node server.js", + "test": "gulp test" + }, + "repository": { + "type": "git", + "url": "git://github.com/Kibibit/kibibit-code-editor.git" }, "author": "neilkalman@gmail.com", "license": "BSD-2-Clause", + "engines": { + "node": "4.2.3", + "npm": "3.8.2" + }, "dependencies": { "bcrypt-nodejs": "0.0.3", - "body-parser": "~1.13.3", + "body-parser": "^1.14.2", "colors": "~1.1.2", "directory-tree": "~0.1.1", - "explicit-object-mapper": "~1.0.0", "express": "~4.13.3", - "fs-extra": "~0.24.0", - "gulp": "~3.9.0", - "jsonwebtoken": "~5.0.5", - "mongoose": "~4.1.6", - "rename-keys": "~1.1.2", + "mime-types": "^2.1.11", "scribe-js": "~2.0.4", "serve-favicon": "~2.3.0", "user-home": "^2.0.0" }, "devDependencies": { - "jshint-stylish": "~2.0.1", - "gulp-jshint": "~1.11.2", - "gulp-beautify": "~1.1.2", + "bower": "^1.7.2", + "gulp": "~3.9.0", + "gulp-cached": "^1.1.0", + "gulp-concat": "~2.6.0", + "gulp-cssnano": "^2.1.0", + "gulp-depcheck": "^1.1.0", + "gulp-develop-server": "~0.5.0", + "gulp-help": "^1.6.1", "gulp-jsbeautifier": "~1.0.1", - "gulp-sass": "~2.0.4", + "gulp-jscs": "~3.0.2", + "gulp-livereload": "~3.8.1", "gulp-rename": "~1.2.2", - "gulp-minify-css": "~1.2.1", - "gulp-sourcemaps": "~1.5.2" + "gulp-sass": "~2.1.1", + "gulp-shell": "~0.5.1", + "gulp-sourcemaps": "~1.6.0", + "gulp-util": "~3.0.7", + "jasmine-core": "~2.4.1", + "jscs": "~2.9.0", + "jscs-angular": "~1.2.1", + "karma": "~0.13.16", + "karma-jasmine": "~0.3.6", + "karma-phantomjs-launcher": "~1.0.0", + "phantomjs-prebuilt": "^2.1.3", + "yargs": "~3.32.0" } } diff --git a/printLogo.js b/printLogo.js new file mode 100644 index 00000000..03450a31 --- /dev/null +++ b/printLogo.js @@ -0,0 +1,19 @@ +module.exports = function() { + var colors = require('colors'); + console.log(); + console.log(' ___ ___ ___ ___'.red); + console.log(' /\\__\\ ___ /\\ \\ ___ /\\ \\ ___ /\\ \\'.red); + console.log(' /'.red + ':'.white + '/ / /\\ \\ /'.red + '::'.white + '\\ \\ /\\ \\ /'.red + '::'.white + '\\ \\ /\\ \\ \\'.red + ':'.white + '\\ \\'.red); + console.log(' /'.red + ':'.white + '/__/ \\'.red + ':'.white + '\\ \\ /'.red + ':'.white + '/\\'.red + ':'.white + '\\ \\ \\'.red + ':'.white + '\\ \\ /'.red + ':'.white + '/\\'.red + ':'.white + '\\ \\ \\'.red + ':'.white + '\\ \\ \\'.red + ':'.white + '\\ \\'.red); + console.log(' /'.red + '::'.white + '\\__\\____ /'.red + '::'.white + '\\__\\ /'.red + '::'.white + '\\~\\'.red + ':'.white + '\\__\\ /'.red + '::'.white + '\\__\\ /'.red + '::'.white + '\\~\\'.red + ':'.white + '\\__\\ /'.red + '::'.white + '\\__\\ /'.red + '::'.white + '\\ \\'.red); + console.log(' /'.red + ':'.white + '/\\'.red + ':::::'.white + '\\__\\ __/'.red + ':'.white + '/\\/__/ /'.red + ':'.white + '/\\'.red + ':'.white + '\\ \\'.red + ':'.white + '|__| __/'.red + ':'.white + '/\\/__/ /'.red + ':'.white + '/\\'.red + ':'.white + '\\ \\'.red + ':'.white + '|__| __/'.red + ':'.white + '/\\/__/ /'.red + ':'.white + '/\\'.red + ':'.white + '\\__\\'.red); + console.log(' \\/_|'.red + ':'.white + '|~~|~ /\\/'.red + ':'.white + '/ / \\'.red + ':'.white + '\\~\\'.red + ':'.white + '\\/'.red + ':'.white + '/ / /\\/'.red + ':'.white + '/ / \\'.red + ':'.white + '\\~\\'.red + ':'.white + '\\/'.red + ':'.white + '/ / /\\/'.red + ':'.white + '/ / /'.red + ':'.white + '/ \\/__/'.red); + console.log(' |'.red + ':'.white + '| | \\'.red + '::'.white + '/__/ \\'.red + ':'.white + '\\ \\'.red + '::'.white + '/ / \\'.red + '::'.white + '/__/ \\'.red + ':'.white + '\\ \\'.red + '::'.white + '/ / \\'.red + '::'.white + '/__/ /'.red + ':'.white + '/ /'.red); + console.log(' |'.red + ':'.white + '| | \\'.red + ':'.white + '\\__\\ \\'.red + ':'.white + '\\/'.red + ':'.white + '/ / \\'.red + ':'.white + '\\__\\ \\'.red + ':'.white + '\\/'.red + ':'.white + '/ / \\'.red + ':'.white + '\\__\\ \\/__/'.red); + console.log(' \\|__| \\/__/ \\'.red + '::'.white + '/__/ \\/__/ \\'.red + '::'.white + '/__/ \\/__/'.red); + console.log(); + console.log('MIT license, 2015-2016'.red); + console.log('https://github.com/Kibibit/kibibit-code-editor'.red); + console.log(); + console.log(); +} diff --git a/public/app/app.js b/public/app/app.js index fd702dd7..b62a33d3 100644 --- a/public/app/app.js +++ b/public/app/app.js @@ -1,9 +1,83 @@ -angular.module('userApp', ['ngAnimate', 'app.routes', 'authService', 'mainCtrl', 'userCtrl', 'userService', 'treeControl']) +angular.module('kibibitCodeEditor', + ['angular-loading-bar', + 'ngAnimate', + 'app.routes', + 'treeControl', + 'ui.ace', + 'angularResizable', + 'ngDialog', + 'ngMaterial', + 'FBAngular', + 'ui.layout', + 'ngScrollbars', + 'ngSanitize', + 'hc.marked', + 'emoji', + 'jsonFormatter', + 'ngclipboard', + 'ng.deviceDetector']) -// application configuration to integrate token into requests -.config(function($httpProvider) { +.config(['$compileProvider', function($compileProvider) { + $compileProvider.debugInfoEnabled(false); +}]) - // attach our auth interceptor to the http requests - $httpProvider.interceptors.push('AuthInterceptor'); +.config(['ScrollBarsProvider', function(ScrollBarsProvider) { + // the following settings are defined for all scrollbars unless the + // scrollbar has local scope configuration + ScrollBarsProvider.defaults = { + scrollButtons: { + scrollAmount: 'auto', // scroll amount when button pressed + enable: false // enable scrolling buttons by default + }, + scrollInertia: 400, // adjust however you want + axis: 'yx', // enable 2 axis scrollbars by default, + theme: 'minimal-dark', + autoHideScrollbar: true + }; +}]) +.config(['markedProvider', function(markedProvider) { + markedProvider.setOptions({ + gfm: true, + tables: true, + highlight: function(code, lang) { + if (lang) { + try { + return hljs.highlight(lang, code, true).value; + } catch (e) { + return hljs.highlightAuto(code).value; + } + } else { + return hljs.highlightAuto(code).value; + } + } + }); +}]) + +.filter('marked', ['marked', function(marked) { + return function(input) { + return marked(input || ''); + }; +}]) + +.constant('CODE_EDITOR', { + 'MODE_LIST': ace.require('ace/ext/modelist'), + 'THEME_LIST': ace.require('ace/ext/themelist') +}) + +.constant('ERROR_MSGS', { + 'TYPE_ERROR': function(varName, typeExpected, typeRecieved) { + return [ + varName, + ' should be a ', + typeExpected, + ' but was given a ', + typeRecieved].join(''); + }, + 'MATCH_ERROR': function(varName, collectionName) { + return [ + varName, + 'value should exist in ', + collectionName].join(''); + } }); diff --git a/public/app/app.routes.js b/public/app/app.routes.js index fc7ad4aa..36ee7238 100644 --- a/public/app/app.routes.js +++ b/public/app/app.routes.js @@ -2,42 +2,42 @@ angular.module('app.routes', ['ngRoute']) .config(function($routeProvider, $locationProvider) { - $routeProvider + $routeProvider - // route for the home page + // route for the home page .when('/', { - templateUrl: 'app/views/pages/home.html' - }) + templateUrl: 'app/views/home.html' + }) - // login page + // login page .when('/login', { - templateUrl: 'app/views/pages/login.html', - controller: 'mainController', - controllerAs: 'login' + templateUrl: 'app/views/login.html', + controller: 'mainController', + controllerAs: 'login' }) // show all users .when('/users', { - templateUrl: 'app/views/pages/users/all.html', - controller: 'userController', - controllerAs: 'user' + templateUrl: 'app/views/users/all.html', + controller: 'userController', + controllerAs: 'user' }) // form to create a new user // same view as edit page .when('/users/create', { - templateUrl: 'app/views/pages/users/single.html', - controller: 'userCreateController', - controllerAs: 'user' + templateUrl: 'app/views/users/single.html', + controller: 'userCreateController', + controllerAs: 'user' }) // page to edit a user .when('/users/:user_id', { - templateUrl: 'app/views/pages/users/single.html', - controller: 'userEditController', - controllerAs: 'user' + templateUrl: 'app/views/users/single.html', + controller: 'userEditController', + controllerAs: 'user' }); - $locationProvider.html5Mode(true); + $locationProvider.html5Mode(true); }); diff --git a/public/app/components/codeEditor/codeEditor.js b/public/app/components/codeEditor/codeEditor.js new file mode 100644 index 00000000..1f898fa3 --- /dev/null +++ b/public/app/components/codeEditor/codeEditor.js @@ -0,0 +1,141 @@ +angular.module('kibibitCodeEditor') + +.directive('kbCodeEditor', function() { + return { + scope: {}, + bindToController: { + openFile: '=kbOpenFile' + }, + controller: 'codeEditorController', + controllerAs: 'codeEditorCtrl', + templateUrl: 'app/components/codeEditor/codeEditorTemplate.html', + link: function(scope, element, attrs, codeEditorCtrl) { + scope.$watch('codeEditorCtrl.openFile', function(newOpenFile) { + codeEditorCtrl.updateFileContent(newOpenFile); + }); + } + }; +}) + +.controller('codeEditorController', [ + '$timeout', + 'FileService', + 'SettingsService', + 'JSONFormatterConfig', + function( + $timeout, + FileService, + SettingsService, + JSONFormatterConfig) { + + var vm = this; + var editor; + var editorSettings = SettingsService.settings.editorSettings; + var minimap; + + // config JSON params + JSONFormatterConfig.hoverPreviewEnabled = true; + JSONFormatterConfig.hoverPreviewArrayCount = 100; + JSONFormatterConfig.hoverPreviewFieldCount = 5; + + var init = function(settings) { + editor.setOptions({ + 'wrap': settings.lineWrap, + 'mode': 'ace/mode/' + settings.syntaxMode, + 'theme': 'ace/theme/' + settings.theme, + 'tabSize': settings.tabWidth, + 'fontSize': settings.fontSize, + 'showGutter': settings.isGutter, + 'useSoftTabs': settings.isSoftTabs, + 'showPrintMargin': settings.ruler + }); + }; + + vm.syntaxMode = function() { return editorSettings.syntaxMode; }; + + // initialize the editor session + vm.aceLoaded = function(_editor) { + editor = _editor; + vm.aceSession = editor.getSession(); + vm.undoManager = editor.getSession().getUndoManager(); + SettingsService.settings.currentUndoManager = vm.undoManager; + SettingsService.settings.currentEditor = editor; + init(editorSettings); + // save cursor position + editor.on('changeSelection', function() { + $timeout(function() { + var cursor = editor.selection.getCursor(); + cursor.row++; + SettingsService.settings.cursor = cursor; + }); + }); + }; + + // save the content of the editor on-change + vm.aceChanged = function(_editor) { + vm.aceDocumentValue = vm.aceSession.getDocument().getValue(); + parseJson(); + }; + + vm.attachedEditorFunctions = { + onLoad: vm.aceLoaded, + onChange: vm.aceChanged + }; + + vm.minimapAdded = function(_minimap) { + minimap = _minimap; + _minimap.setOptions({ + highlightActiveLine: false, + readOnly: true, + showGutter: false, + displayIndentGuides: false, + fontSize: 6, //NOTICE: this is set to 6 but in the css is set to 4px to have a little margin between lines + theme: 'ace/theme/' + editorSettings.theme, + mode: 'ace/mode/' + editorSettings.syntaxMode + }); + }; + + vm.minimapChanged = function(_minimap) { + minimap.setOptions({ + theme: 'ace/theme/' + editorSettings.theme, + mode: 'ace/mode/' + editorSettings.syntaxMode + }); + }; + + vm.updateFileContent = function(filePath) { + if (filePath !== '') { + FileService.getFile(filePath, function(fileInfo) { + vm.fileInfo = fileInfo.data; + vm.code = vm.fileInfo.content; + var fileMode = getModeFromMimeType(vm.fileInfo); + editorSettings.syntaxMode = fileMode; + vm.parsedJson = undefined; + console.debug('changed mode to ' + fileMode); + }); + } + }; + + vm.shouldShowCompiledView = function() { + vm.showCompiledView = + editorSettings.syntaxMode === 'json' || + editorSettings.syntaxMode === 'markdown'; + return vm.showCompiledView; + }; + + function parseJson() { + // the parsedJson variable won't update if the json is invalid + if (editorSettings.syntaxMode === 'json') { + try { + vm.parsedJson = JSON.parse(vm.aceDocumentValue); + } catch (e) {} + } + }; + + function getModeFromMimeType(file) { + var getModeRegex = /\/(x-)?(.*)$/; + return file && file.mimeType ? + file.mimeType.match(getModeRegex)[2] : + 'text'; + } + } +]); diff --git a/public/app/components/codeEditor/codeEditorTemplate.html b/public/app/components/codeEditor/codeEditorTemplate.html new file mode 100644 index 00000000..0504e22d --- /dev/null +++ b/public/app/components/codeEditor/codeEditorTemplate.html @@ -0,0 +1,10 @@ +
+ +' + message + '
'); + + $('.new-output').velocity( + 'scroll' + ), {duration: 100}; +} + + function showKittens() { + $('.terminal').append(',----, ,----, ,---,
' + + ',--. ,/ .`| ,/ .`| ,--. ,`--.\' |
' + + ',--/ /| ,---, ,` .\' : ,` .\' : ,---,. ,--.\'| .--.--. | : :
' + + ',---,\': / \' ,`--.\' | ; ; / ; ; / ,\' .\' | ,--,: : | / / \'. \' \' ;
' + + ': : \'/ / | : : .\'___,/ ,\' .\'___,/ ,\' ,---.\' | ,`--.\'`| \' : | : /`. / | | |
' + + '| \' , : | \' | : | | : | | | .\' | : : | | ; | |--` \' : ;
' + + '\' | / | : | ; |.\'; ; ; |.\'; ; : : |-, : | \\ | : | : ;_ | | \'
' + + '| ; ; \' \' ; `----\' | | `----\' | | : | ;/| | : \' \'; | \\ \\ `. \' : |
' + + ': \' \\ | | | \' : ; \' : ; | : .\' \' \' ;. ; `----. \\ ; | ;
' + + '\' : |. \\ | | \' \' : | \' : | \' : ;/| \' : | ; .\' / /`--\' / `--..`;
' + + '| | \'_\\.\' \' : | ; |.\' ; |.\' | | \\ | | \'`--\' \'--\'. / .--,_
' + + '\' : | ; |.\' \'---\' \'---\' | : .\' \' : | `--\'---\' | |`.
' + + '; |,\' \'---\' | | ,\' ; |.\' `-- -`, ;
' + + '\'---\' `----\' \'---\' \'---`\'
' + + '
oops... something went wrong.
+The file you requested cannot be found right meow. Try typing 'kittens'.
+ +You triggered the "' + name + '" action
', + plain: true + }); + }; + + vm.settings = SettingsService.settings; + + vm.hasUndo = function() { + if (vm.settings.currentUndoManager && + vm.settings.currentUndoManager.hasUndo) { + vm.enableUndo = vm.settings.currentUndoManager.hasUndo(); + return vm.enableUndo; + } else { + return false; + } + }; + + vm.hasRedo = function() { + if (vm.settings.currentUndoManager && + vm.settings.currentUndoManager.hasRedo) { + vm.enableRedo = vm.settings.currentUndoManager.hasRedo(); + return vm.enableRedo; + } else { + return false; + } + }; + + vm.cutSelection = function(e) { + vm.settings.currentEditor.session.replace( + vm.settings.currentEditor.selection.getRange(), ''); + + vm.settings.currentEditor.focus(); + }; + + vm.toggleFullscreen = function() { + vm.settings.isFullscreen = !vm.settings.isFullscreen; + }; + + vm.toggleLineWrap = function() { + vm.settings.editorSettings.lineWrap = !vm.settings.editorSettings.lineWrap; + }; + + vm.toggleSoftTabs = function() { + var editorSettings = vm.settings.editorSettings; + editorSettings.isSoftTabs = !editorSettings.isSoftTabs; + }; + + vm.toggleGutter = function() { + var editorSettings = vm.settings.editorSettings; + editorSettings.isGutter = !editorSettings.isGutter; + }; + +}); diff --git a/public/app/components/menuBar/menuBarTemplate.html b/public/app/components/menuBar/menuBarTemplate.html new file mode 100644 index 00000000..2afeb220 --- /dev/null +++ b/public/app/components/menuBar/menuBarTemplate.html @@ -0,0 +1,295 @@ +