mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-12-25 03:44:29 +03:00
Merge branch 'master' into ember
Conflicts: Gruntfile.js core/client/assets/lib/showdown/extensions/ghostdown.js core/client/views/editor.js core/clientold/assets/lib/showdown/extensions/ghostdown.js core/shared/lib/showdown/extensions/ghostimagepreview.js core/test/unit/showdown_client_integrated_spec.js core/test/unit/showdown_ghostimagepreview_spec.js
This commit is contained in:
commit
c746a88b2e
2
.gitmodules
vendored
2
.gitmodules
vendored
@ -1,3 +1,3 @@
|
||||
[submodule "content/themes/casper"]
|
||||
path = content/themes/casper
|
||||
url = git://github.com/TryGhost/Casper.git
|
||||
url = https://github.com/TryGhost/Casper.git
|
||||
|
12
.travis.yml
12
.travis.yml
@ -2,20 +2,20 @@ language: node_js
|
||||
node_js:
|
||||
- "0.10"
|
||||
env:
|
||||
- DB=sqlite3
|
||||
- DB=mysql
|
||||
- DB=pg
|
||||
- DB=sqlite3 NODE_ENV=testing
|
||||
- DB=mysql NODE_ENV=testing-mysql
|
||||
- DB=pg NODE_ENV=testing-pg
|
||||
matrix:
|
||||
allow_failures:
|
||||
- env: DB=pg
|
||||
- env: DB=pg NODE_ENV=testing-pg
|
||||
before_install:
|
||||
- git clone git://github.com/n1k0/casperjs.git ~/casperjs
|
||||
- cd ~/casperjs
|
||||
- git checkout tags/1.1-beta3
|
||||
- export PATH=$PATH:`pwd`/bin
|
||||
- cd -
|
||||
- if [ $DB == "mysql" ]; then mysql -e 'create database ghost_travis'; fi
|
||||
- if [ $DB == "pg" ]; then npm install pg; psql -c 'create database ghost_travis;' -U postgres; fi
|
||||
- if [ $DB == "mysql" ]; then mysql -e 'create database ghost_testing'; fi
|
||||
- if [ $DB == "pg" ]; then npm install pg; psql -c 'create database ghost_testing;' -U postgres; fi
|
||||
before_script:
|
||||
- phantomjs --version
|
||||
- casperjs --version
|
||||
|
@ -55,14 +55,14 @@ Guidelines for bug reports:
|
||||
helpful thing in the world is if we can *see* what you're talking about.
|
||||
Use [LICEcap](http://www.cockos.com/licecap/) to quickly and easily record a short screencast (24fps) and save it as an animated gif! Embed it directly into your GitHub issue. Kapow.
|
||||
|
||||
5. Use the Bug Report template below or [click this link](https://github.com/TryGhost/Ghost/issues/new?title=Bug%3A&body=%23%23%23%20Issue%20Summary%0A%0A%23%23%23%20Steps%20to%20Reproduce%0A%0A1.%20This%20is%20the%20first%20step%0A%0AThis%20is%20a%20bug%20because...%0A%0A%23%23%23%20Technical%20details%0A%0A*%20Ghost%20Version%3A%20master%20-%20latest%20commit%3A%20%20INSERT%20COMMIT%20REF%0A*%20Client%20OS%3A%20%0A*%20Server%20OS%3A%20%0A*%20Node%20Version%3A%20%0A*%20Browser%3A) to start creating a bug report with the template automatically.
|
||||
5. Use the Bug Report template below or [click this link](https://github.com/TryGhost/Ghost/issues/new?title=Bug%3A&body=%23%23%23%20Issue%20Summary%0A%0A%23%23%23%20Steps%20to%20Reproduce%0A%0A1.%20This%20is%20the%20first%20step%0A%0AThis%20is%20a%20bug%20because...%0A%0A%23%23%23%20Technical%20details%0A%0A*%20Ghost%20Version%3A%20master%20-%20latest%20commit%3A%20%20INSERT%20COMMIT%20REF%0A*%20Client%20OS%3A%20%0A*%20Server%20OS%3A%20%0A*%20Node%20Version%3A%20%0A*%20Browser%3A%20%0A*%20Database%3A) to start creating a bug report with the template automatically.
|
||||
|
||||
A good bug report shouldn't leave others needing to chase you up for more information. Be sure to include the
|
||||
details of your environment.
|
||||
|
||||
Here is a [real example](https://github.com/TryGhost/Ghost/issues/413)
|
||||
|
||||
Template Example ([click to use](https://github.com/TryGhost/Ghost/issues/new?title=Bug%3A&body=%23%23%23%20Issue%20Summary%0A%0A%23%23%23%20Steps%20to%20Reproduce%0A%0A1.%20This%20is%20the%20first%20step%0A%0AThis%20is%20a%20bug%20because...%0A%0A%23%23%23%20Technical%20details%0A%0A*%20Ghost%20Version%3A%20master%20-%20latest%20commit%3A%20%20INSERT%20COMMIT%20REF%0A*%20Client%20OS%3A%20%0A*%20Server%20OS%3A%20%0A*%20Node%20Version%3A%20%0A*%20Browser%3A)):
|
||||
Template Example ([click to use](https://github.com/TryGhost/Ghost/issues/new?title=Bug%3A&body=%23%23%23%20Issue%20Summary%0A%0A%23%23%23%20Steps%20to%20Reproduce%0A%0A1.%20This%20is%20the%20first%20step%0A%0AThis%20is%20a%20bug%20because...%0A%0A%23%23%23%20Technical%20details%0A%0A*%20Ghost%20Version%3A%20master%20-%20latest%20commit%3A%20%20INSERT%20COMMIT%20REF%0A*%20Client%20OS%3A%20%0A*%20Server%20OS%3A%20%0A*%20Node%20Version%3A%20%0A*%20Browser%3A%20%0A*%20Database%3A)):
|
||||
```
|
||||
Short and descriptive example bug report title
|
||||
|
||||
@ -87,6 +87,7 @@ reported. Especially, why do you consider this to be a bug? What do you expect t
|
||||
* Server OS: CentOS 6.4
|
||||
* Node Version: 0.10.16
|
||||
* Browser: Chrome 29.0.1547.57
|
||||
* Database: SQLite / MySQL / postgres
|
||||
```
|
||||
|
||||
<a name="features"></a>
|
||||
@ -190,12 +191,13 @@ developing Ghost.
|
||||
1. Check you have the pre-requisites listed above!
|
||||
1. Clone the git repo
|
||||
1. cd into the project folder
|
||||
1. Run `npm install -g grunt-cli`
|
||||
1. Run `npm install`.
|
||||
1. Run `npm install -g grunt-cli` - to make it possible to run grunt commands
|
||||
1. Run `npm install` - you need all the dependencies, so do not use the `--production` flag mentioned in user install guides
|
||||
* If the install fails with errors to do with "node-gyp rebuild" or "SQLite3", follow the SQLite3 install
|
||||
instructions below this list
|
||||
* Usually if you're within vagrant, and have installed the guest plugins and updated that, this will not happen
|
||||
1. Run `grunt init` from the root - copies assets and compiles Handlebars templates
|
||||
1. Run `grunt init` from the root - updates bower dependencies, copies assets and compiles Handlebars templates
|
||||
1. If you're going to run in production mode, you also need to run `grunt prod`
|
||||
1. Run `npm start` from the root to start the server.
|
||||
|
||||
If something goes wrong, please see the
|
||||
@ -204,7 +206,7 @@ If something goes wrong, please see the
|
||||
### Developer Tips
|
||||
Whilst developing, you can take advantage of the [Grunt toolkit](https://github.com/TryGhost/Ghost/wiki/Grunt-Toolkit) to automatically compile assets, such as handlebar templates, stylesheets and javascripts. Some useful commands include:
|
||||
- `grunt dev` => Automatically compile assets in development environment
|
||||
- `grunt prod` => Automatically compile assets in production environment
|
||||
- `grunt prod` => Automatically compile assets for the production environment
|
||||
- `grunt watch` => Automatically compile handlebars
|
||||
|
||||
Addresses for development:
|
||||
@ -218,15 +220,15 @@ or more of the following:
|
||||
|
||||
* `npm install` - fetch any new dependencies
|
||||
* `git submodule update` - fetch the latest changes to Casper (the default theme)
|
||||
* `grunt` - will recompile handlebars templates for the admin (as long as you have previously
|
||||
run `grunt init` to install bower dependencies)
|
||||
* `grunt init` - will fetch bower dependencies and recompile handlebars templates for the admin
|
||||
* delete content/data/*.db - delete the database and allow Ghost to recreate the fixtures
|
||||
|
||||
### Key Branches & Tags
|
||||
|
||||
- **[master](https://github.com/TryGhost/Ghost)** is the bleeding edge development branch. All work on the next
|
||||
release is here.
|
||||
- **[gh-pages](http://tryghost.github.io/Ghost)** is The Ghost Guide documentation for Getting Started with Ghost.
|
||||
release is here. Do **NOT** use this branch for a production site.
|
||||
- **[stable](https://github.com/TryGhost/Ghost/tree/stable)** contains the latest release of Ghost. This branch may be used in production.
|
||||
- **[gh-pages](http://github.com/TryGhost/Ghost/tree/gh-pages)** contains [The Ghost Guide](http://docs.ghost.org) documentation for Getting Started with Ghost.
|
||||
|
||||
|
||||
## Grunt Toolkit
|
||||
@ -245,7 +247,7 @@ When cloning from GitHub be sure to use SSH and to run `git submodule update --i
|
||||
|
||||
### Ghost doesn't do anything - I get a blank screen
|
||||
|
||||
Sounds like you probably didn't run the right grunt command for building assets
|
||||
Sounds like you probably didn't run the right grunt command for building assets. You may need to run `grunt init` and if using production mode, `grunt prod` as well.
|
||||
|
||||
### SQLite3 doesn't install properly during npm install
|
||||
|
||||
|
66
Gruntfile.js
66
Gruntfile.js
@ -65,21 +65,25 @@ var path = require('path'),
|
||||
concat: {
|
||||
files: [
|
||||
'core/clientold/*.js',
|
||||
'core/clientold/helpers/*.js',
|
||||
'core/clientold/models/*.js',
|
||||
'core/clientold/tpl/*.js',
|
||||
'core/clientold/views/*.js'
|
||||
'core/clientold/**/*.js'
|
||||
],
|
||||
tasks: ['concat']
|
||||
},
|
||||
'ghost-ui': {
|
||||
files: [
|
||||
// Ghost UI CSS
|
||||
'bower_components/ghost-ui/dist/css/*.css'
|
||||
],
|
||||
tasks: ['copy:dev']
|
||||
},
|
||||
livereload: {
|
||||
files: [
|
||||
// Theme CSS
|
||||
'content/themes/casper/css/*.css',
|
||||
// Ghost UI CSS
|
||||
'bower_components/ghost-ui/dist/css/*.css',
|
||||
// Theme JS
|
||||
'content/themes/casper/js/*.js',
|
||||
// Client CSS
|
||||
'core/client/assets/css/*.css',
|
||||
// Admin JS
|
||||
'core/built/scripts/*.js'
|
||||
],
|
||||
@ -300,12 +304,8 @@ var path = require('path'),
|
||||
]
|
||||
},
|
||||
|
||||
api: {
|
||||
src: ['core/test/functional/api/*_test.js']
|
||||
},
|
||||
|
||||
routes: {
|
||||
src: ['core/test/functional/routes/*_test.js']
|
||||
src: ['core/test/functional/routes/**/*_test.js']
|
||||
}
|
||||
},
|
||||
|
||||
@ -534,9 +534,8 @@ var path = require('path'),
|
||||
'bower_components/showdown/src/showdown.js',
|
||||
'bower_components/validator-js/validator.js',
|
||||
|
||||
'core/clientold/assets/lib/showdown/extensions/ghostdown.js',
|
||||
'core/shared/lib/showdown/extensions/typography.js',
|
||||
'core/shared/lib/showdown/extensions/github.js',
|
||||
'core/shared/lib/showdown/extensions/ghostimagepreview.js',
|
||||
'core/shared/lib/showdown/extensions/ghostgfm.js',
|
||||
|
||||
// ToDo: Remove or replace
|
||||
'core/clientold/assets/vendor/shortcuts.js',
|
||||
@ -553,7 +552,14 @@ var path = require('path'),
|
||||
'core/clientold/mobile-interactions.js',
|
||||
'core/clientold/toggle.js',
|
||||
'core/clientold/markdown-actions.js',
|
||||
'core/clientold/helpers/index.js'
|
||||
'core/clientold/helpers/index.js',
|
||||
'core/clientold/assets/lib/editor/index.js',
|
||||
'core/clientold/assets/lib/editor/markerManager.js',
|
||||
'core/clientold/assets/lib/editor/uploadManager.js',
|
||||
'core/clientold/assets/lib/editor/markdownEditor.js',
|
||||
'core/clientold/assets/lib/editor/htmlPreview.js',
|
||||
'core/clientold/assets/lib/editor/scrollHandler.js',
|
||||
'core/clientold/assets/lib/editor/mobileCodeMirror.js'
|
||||
],
|
||||
|
||||
'core/built/scripts/templates.js': [
|
||||
@ -587,9 +593,8 @@ var path = require('path'),
|
||||
'bower_components/showdown/src/showdown.js',
|
||||
'bower_components/moment/moment.js',
|
||||
|
||||
'core/clientold/assets/lib/showdown/extensions/ghostdown.js',
|
||||
'core/shared/lib/showdown/extensions/typography.js',
|
||||
'core/shared/lib/showdown/extensions/github.js'
|
||||
'core/shared/lib/showdown/extensions/ghostimagepreview.js',
|
||||
'core/shared/lib/showdown/extensions/ghostgfm.js',
|
||||
]
|
||||
}
|
||||
},
|
||||
@ -613,9 +618,8 @@ var path = require('path'),
|
||||
'bower_components/showdown/src/showdown.js',
|
||||
'bower_components/validator-js/validator.js',
|
||||
|
||||
'core/clientold/assets/lib/showdown/extensions/ghostdown.js',
|
||||
'core/shared/lib/showdown/extensions/typography.js',
|
||||
'core/shared/lib/showdown/extensions/github.js',
|
||||
'core/shared/lib/showdown/extensions/ghostimagepreview.js',
|
||||
'core/shared/lib/showdown/extensions/ghostgfm.js',
|
||||
|
||||
// ToDo: Remove or replace
|
||||
'core/clientold/assets/vendor/shortcuts.js',
|
||||
@ -632,6 +636,14 @@ var path = require('path'),
|
||||
'core/clientold/markdown-actions.js',
|
||||
'core/clientold/helpers/index.js',
|
||||
|
||||
'core/clientold/assets/lib/editor/index.js',
|
||||
'core/clientold/assets/lib/editor/markerManager.js',
|
||||
'core/clientold/assets/lib/editor/uploadManager.js',
|
||||
'core/clientold/assets/lib/editor/markdownEditor.js',
|
||||
'core/clientold/assets/lib/editor/htmlPreview.js',
|
||||
'core/clientold/assets/lib/editor/scrollHandler.js',
|
||||
'core/clientold/assets/lib/editor/mobileCodeMirror.js',
|
||||
|
||||
'core/clientold/tpl/hbs-tpl.js',
|
||||
|
||||
'core/clientold/models/**/*.js',
|
||||
@ -649,7 +661,8 @@ var path = require('path'),
|
||||
uglify: {
|
||||
prod: {
|
||||
files: {
|
||||
'core/built/scripts/ghost.min.js': 'core/built/scripts/ghost.js'
|
||||
'core/built/scripts/ghost.min.js': 'core/built/scripts/ghost.js',
|
||||
'core/built/public/jquery.min.js': 'core/built/public/jquery.js'
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -657,11 +670,10 @@ var path = require('path'),
|
||||
|
||||
grunt.initConfig(cfg);
|
||||
|
||||
|
||||
// ## Custom Tasks
|
||||
|
||||
grunt.registerTask('setTestEnv', 'Use "testing" Ghost config; unless we are running on travis (then show queries for debugging)', function () {
|
||||
process.env.NODE_ENV = process.env.TRAVIS ? 'travis-' + process.env.DB : 'testing';
|
||||
process.env.NODE_ENV = process.env.TRAVIS ? process.env.NODE_ENV : 'testing';
|
||||
cfg.express.test.options.node_env = process.env.NODE_ENV;
|
||||
});
|
||||
|
||||
@ -994,11 +1006,9 @@ var path = require('path'),
|
||||
|
||||
grunt.registerTask('test-functional', 'Run functional interface tests (CasperJS)', ['clean:test', 'setTestEnv', 'loadConfig', 'copy:dev', 'express:test', 'spawn-casperjs', 'express:test:stop']);
|
||||
|
||||
grunt.registerTask('test-api', 'Run functional api tests (mocha)', ['clean:test', 'setTestEnv', 'loadConfig', 'express:test', 'mochacli:api', 'express:test:stop']);
|
||||
grunt.registerTask('test-routes', 'Run functional route tests (mocha)', ['clean:test', 'setTestEnv', 'loadConfig', 'mochacli:routes']);
|
||||
|
||||
grunt.registerTask('test-routes', 'Run functional route tests (mocha)', ['clean:test', 'setTestEnv', 'loadConfig', 'express:test', 'mochacli:routes', 'express:test:stop']);
|
||||
|
||||
grunt.registerTask('validate', 'Run tests and lint code', ['jshint', 'test-routes', 'test-unit', 'test-api', 'test-integration', 'test-functional']);
|
||||
grunt.registerTask('validate', 'Run tests and lint code', ['jshint', 'test-routes', 'test-unit', 'test-integration', 'test-functional']);
|
||||
|
||||
|
||||
// ### Coverage report for Unit and Integration Tests
|
||||
|
18
README.md
18
README.md
@ -1,4 +1,4 @@
|
||||
# [Ghost](https://github.com/TryGhost/Ghost) [![Build Status](https://travis-ci.org/TryGhost/Ghost.png?branch=master)](https://travis-ci.org/TryGhost/Ghost)
|
||||
# [Ghost](https://github.com/TryGhost/Ghost) [![Build Status](https://travis-ci.org/TryGhost/Ghost.svg?branch=master)](https://travis-ci.org/TryGhost/Ghost)
|
||||
|
||||
Ghost is a free, open, simple blogging platform that's available to anyone who wants to use it. Lovingly created and maintained by [John O'Nolan](http://twitter.com/JohnONolan) + [Hannah Wolfe](http://twitter.com/ErisDS) + an amazing group of [contributors](https://github.com/TryGhost/Ghost/contributors).
|
||||
|
||||
@ -8,8 +8,6 @@ Visit the project's website at <http://ghost.org> • docs on <http://docs.gh
|
||||
|
||||
Want to report a bug, request a feature, or help us build or translate Ghost? Check out our in depth guide to [Contributing to Ghost](https://github.com/TryGhost/Ghost/blob/master/CONTRIBUTING.md). We need all the help we can get! You can also join in with our [community](https://github.com/TryGhost/Ghost#community) to keep up-to-date and meet other Ghosters.
|
||||
|
||||
|
||||
|
||||
## Getting Started
|
||||
|
||||
There are **two** main ways to get started with Ghost, take care to use the method which best suits your needs.
|
||||
@ -39,8 +37,18 @@ Check out the [Documentation](http://docs.ghost.org/) for more detailed instruct
|
||||
|
||||
If you're a theme, app or core developer, or someone comfortable getting up and running from a `git clone`, this method is for you.
|
||||
|
||||
If you clone the GitHub repository, you will need to build a number of assets, such as SASS and JavaScript templates. This requires you to have Ruby and a number of other pre-requisites.
|
||||
Full instructions can be found in the [Contributing Guide](https://github.com/TryGhost/Ghost/blob/master/CONTRIBUTING.md) under the heading "[Working on Ghost Core](https://github.com/TryGhost/Ghost/blob/master/CONTRIBUTING.md#working-on-ghost-core)".
|
||||
If you clone the GitHub repository, you will need to build a number of assets using grunt.
|
||||
|
||||
Please do **NOT** use the master branch of Ghost in production. If you are using git to deploy to production, please use the latest [release](https://github.com/TryGhost/Ghost/releases) or the [stable](https://github.com/TryGhost/Ghost/tree/stable) branch which contains the latest release.
|
||||
|
||||
#### Quickstart:
|
||||
|
||||
1. `npm install -g grunt-cli`
|
||||
1. `npm install`
|
||||
1. `grunt init` (and `grunt prod` if you want to run Ghost in production mode)
|
||||
1. `npm start`
|
||||
|
||||
Full instructions & troubleshooting tips can be found in the [Contributing Guide](https://github.com/TryGhost/Ghost/blob/master/CONTRIBUTING.md) under the heading "[Working on Ghost Core](https://github.com/TryGhost/Ghost/blob/master/CONTRIBUTING.md#working-on-ghost-core)".
|
||||
|
||||
Check out the [Documentation](http://docs.ghost.org/) for more detailed instructions, or get in touch via the [forum](http://ghost.org/forum) if you get stuck.
|
||||
|
||||
|
@ -2,12 +2,12 @@
|
||||
"name": "ghost",
|
||||
"dependencies": {
|
||||
"backbone": "1.0.0",
|
||||
"codemirror": "3.15.0",
|
||||
"codemirror": "4.0.1",
|
||||
"Countable": "2.0.2",
|
||||
"ember": "1.5.0",
|
||||
"ember-resolver": "git://github.com/stefanpenner/ember-jj-abrams-resolver.git#181251821cf513bb58d3e192faa13245a816f75e",
|
||||
"fastclick": "1.0.0",
|
||||
"ghost-ui": "0.1.2",
|
||||
"ghost-ui": "0.1.3",
|
||||
"handlebars": "1.3.0",
|
||||
"ic-ajax": "1.0.1",
|
||||
"jquery": "1.11.0",
|
||||
@ -17,10 +17,10 @@
|
||||
"lodash": "2.4.1",
|
||||
"moment": "2.4.0",
|
||||
"nprogress": "0.1.2",
|
||||
"showdown": "0.3.1",
|
||||
"showdown": "https://github.com/ErisDS/showdown.git#v0.3.2-ghost",
|
||||
"validator-js": "3.4.0"
|
||||
},
|
||||
"resolutions": {
|
||||
"ember": "~1.4.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -85,34 +85,17 @@ config = {
|
||||
logging: false
|
||||
},
|
||||
|
||||
// ### Travis
|
||||
// Automated testing run through GitHub
|
||||
'travis-sqlite3': {
|
||||
url: 'http://127.0.0.1:2369',
|
||||
database: {
|
||||
client: 'sqlite3',
|
||||
connection: {
|
||||
filename: path.join(__dirname, '/content/data/ghost-travis.db')
|
||||
}
|
||||
},
|
||||
server: {
|
||||
host: '127.0.0.1',
|
||||
port: '2369'
|
||||
},
|
||||
logging: false
|
||||
},
|
||||
|
||||
// ### Travis
|
||||
// Automated testing run through GitHub
|
||||
'travis-mysql': {
|
||||
// ### Testing MySQL
|
||||
// Used by Travis - Automated testing run through GitHub
|
||||
'testing-mysql': {
|
||||
url: 'http://127.0.0.1:2369',
|
||||
database: {
|
||||
client: 'mysql',
|
||||
connection: {
|
||||
host : '127.0.0.1',
|
||||
user : 'travis',
|
||||
user : 'root',
|
||||
password : '',
|
||||
database : 'ghost_travis',
|
||||
database : 'ghost_testing',
|
||||
charset : 'utf8'
|
||||
}
|
||||
},
|
||||
@ -123,9 +106,9 @@ config = {
|
||||
logging: false
|
||||
},
|
||||
|
||||
// ### Travis
|
||||
// Automated testing run through GitHub
|
||||
'travis-pg': {
|
||||
// ### Testing pg
|
||||
// Used by Travis - Automated testing run through GitHub
|
||||
'testing-pg': {
|
||||
url: 'http://127.0.0.1:2369',
|
||||
database: {
|
||||
client: 'pg',
|
||||
@ -133,7 +116,7 @@ config = {
|
||||
host : '127.0.0.1',
|
||||
user : 'postgres',
|
||||
password : '',
|
||||
database : 'ghost_travis',
|
||||
database : 'ghost_testing',
|
||||
charset : 'utf8'
|
||||
}
|
||||
},
|
||||
|
@ -1 +1 @@
|
||||
Subproject commit 3c1b7d68ec63f7cf7621f3aa8e30b5c01961e145
|
||||
Subproject commit a41588f3e5d0ff5393ec4c70713e8905b9c62526
|
44
core/clientold/assets/lib/editor/htmlPreview.js
Normal file
44
core/clientold/assets/lib/editor/htmlPreview.js
Normal file
@ -0,0 +1,44 @@
|
||||
// # Ghost Editor HTML Preview
|
||||
//
|
||||
// HTML Preview is the right pane in the split view editor.
|
||||
// It is effectively just a scrolling container for the HTML output from showdown
|
||||
// It knows how to update itself, and that's pretty much it.
|
||||
|
||||
/*global Ghost, Showdown, Countable, _, $ */
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
var HTMLPreview = function (markdown, uploadMgr) {
|
||||
var converter = new Showdown.converter({extensions: ['ghostimagepreview', 'ghostgfm']}),
|
||||
preview = document.getElementsByClassName('rendered-markdown')[0],
|
||||
update;
|
||||
|
||||
// Update the preview
|
||||
// Includes replacing all the HTML, intialising upload dropzones, and updating the counter
|
||||
update = function () {
|
||||
preview.innerHTML = converter.makeHtml(markdown.value());
|
||||
|
||||
uploadMgr.enable();
|
||||
|
||||
Countable.once(preview, function (counter) {
|
||||
$('.entry-word-count').text($.pluralize(counter.words, 'word'));
|
||||
$('.entry-character-count').text($.pluralize(counter.characters, 'character'));
|
||||
$('.entry-paragraph-count').text($.pluralize(counter.paragraphs, 'paragraph'));
|
||||
});
|
||||
};
|
||||
|
||||
// Public API
|
||||
_.extend(this, {
|
||||
scrollViewPort: function () {
|
||||
return $('.entry-preview-content');
|
||||
},
|
||||
scrollContent: function () {
|
||||
return $('.rendered-markdown');
|
||||
},
|
||||
update: update
|
||||
});
|
||||
};
|
||||
|
||||
Ghost.Editor = Ghost.Editor || {};
|
||||
Ghost.Editor.HTMLPreview = HTMLPreview;
|
||||
} ());
|
79
core/clientold/assets/lib/editor/index.js
Normal file
79
core/clientold/assets/lib/editor/index.js
Normal file
@ -0,0 +1,79 @@
|
||||
// # Ghost Editor
|
||||
//
|
||||
// Ghost Editor contains a set of modules which make up the editor component
|
||||
// It manages the left and right panes, and all of the communication between them
|
||||
// Including scrolling,
|
||||
|
||||
/*global document, $, _, Ghost */
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
var Editor = function () {
|
||||
var self = this,
|
||||
$document = $(document),
|
||||
// Create all the needed editor components, passing them what they need to function
|
||||
markdown = new Ghost.Editor.MarkdownEditor(),
|
||||
uploadMgr = new Ghost.Editor.UploadManager(markdown),
|
||||
preview = new Ghost.Editor.HTMLPreview(markdown, uploadMgr),
|
||||
scrollHandler = new Ghost.Editor.ScrollHandler(markdown, preview),
|
||||
unloadDirtyMessage,
|
||||
handleChange,
|
||||
handleDrag;
|
||||
|
||||
unloadDirtyMessage = function () {
|
||||
return '==============================\n\n' +
|
||||
'Hey there! It looks like you\'re in the middle of writing' +
|
||||
' something and you haven\'t saved all of your content.' +
|
||||
'\n\nSave before you go!\n\n' +
|
||||
'==============================';
|
||||
};
|
||||
|
||||
handleChange = function () {
|
||||
self.setDirty(true);
|
||||
preview.update();
|
||||
};
|
||||
|
||||
handleDrag = function (e) {
|
||||
e.preventDefault();
|
||||
};
|
||||
|
||||
// Public API
|
||||
_.extend(this, {
|
||||
enable: function () {
|
||||
// Listen for changes
|
||||
$document.on('markdownEditorChange', handleChange);
|
||||
|
||||
// enable editing and scrolling
|
||||
markdown.enable();
|
||||
scrollHandler.enable();
|
||||
},
|
||||
|
||||
disable: function () {
|
||||
// Don't listen for changes
|
||||
$document.off('markdownEditorChange', handleChange);
|
||||
|
||||
// disable editing and scrolling
|
||||
markdown.disable();
|
||||
scrollHandler.disable();
|
||||
},
|
||||
|
||||
// Get the markdown value from the editor for saving
|
||||
// Upload manager makes sure the upload markers are removed beforehand
|
||||
value: function () {
|
||||
return uploadMgr.value();
|
||||
},
|
||||
|
||||
setDirty: function (dirty) {
|
||||
window.onbeforeunload = dirty ? unloadDirtyMessage : null;
|
||||
}
|
||||
});
|
||||
|
||||
// Initialise
|
||||
$document.on('drop dragover', handleDrag);
|
||||
preview.update();
|
||||
this.enable();
|
||||
};
|
||||
|
||||
Ghost.Editor = Ghost.Editor || {};
|
||||
Ghost.Editor.Main = Editor;
|
||||
}());
|
95
core/clientold/assets/lib/editor/markdownEditor.js
Normal file
95
core/clientold/assets/lib/editor/markdownEditor.js
Normal file
@ -0,0 +1,95 @@
|
||||
// # Ghost Editor Markdown Editor
|
||||
//
|
||||
// Markdown Editor is a light wrapper around CodeMirror
|
||||
|
||||
/*global Ghost, CodeMirror, shortcut, _, $ */
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
var MarkdownShortcuts,
|
||||
MarkdownEditor;
|
||||
|
||||
MarkdownShortcuts = [
|
||||
{'key': 'Ctrl+B', 'style': 'bold'},
|
||||
{'key': 'Meta+B', 'style': 'bold'},
|
||||
{'key': 'Ctrl+I', 'style': 'italic'},
|
||||
{'key': 'Meta+I', 'style': 'italic'},
|
||||
{'key': 'Ctrl+Alt+U', 'style': 'strike'},
|
||||
{'key': 'Ctrl+Shift+K', 'style': 'code'},
|
||||
{'key': 'Meta+K', 'style': 'code'},
|
||||
{'key': 'Ctrl+Alt+1', 'style': 'h1'},
|
||||
{'key': 'Ctrl+Alt+2', 'style': 'h2'},
|
||||
{'key': 'Ctrl+Alt+3', 'style': 'h3'},
|
||||
{'key': 'Ctrl+Alt+4', 'style': 'h4'},
|
||||
{'key': 'Ctrl+Alt+5', 'style': 'h5'},
|
||||
{'key': 'Ctrl+Alt+6', 'style': 'h6'},
|
||||
{'key': 'Ctrl+Shift+L', 'style': 'link'},
|
||||
{'key': 'Ctrl+Shift+I', 'style': 'image'},
|
||||
{'key': 'Ctrl+Q', 'style': 'blockquote'},
|
||||
{'key': 'Ctrl+Shift+1', 'style': 'currentDate'},
|
||||
{'key': 'Ctrl+U', 'style': 'uppercase'},
|
||||
{'key': 'Ctrl+Shift+U', 'style': 'lowercase'},
|
||||
{'key': 'Ctrl+Alt+Shift+U', 'style': 'titlecase'},
|
||||
{'key': 'Ctrl+Alt+W', 'style': 'selectword'},
|
||||
{'key': 'Ctrl+L', 'style': 'list'},
|
||||
{'key': 'Ctrl+Alt+C', 'style': 'copyHTML'},
|
||||
{'key': 'Meta+Alt+C', 'style': 'copyHTML'},
|
||||
{'key': 'Meta+Enter', 'style': 'newLine'},
|
||||
{'key': 'Ctrl+Enter', 'style': 'newLine'}
|
||||
];
|
||||
|
||||
MarkdownEditor = function () {
|
||||
var codemirror = CodeMirror.fromTextArea(document.getElementById('entry-markdown'), {
|
||||
mode: 'gfm',
|
||||
tabMode: 'indent',
|
||||
tabindex: '2',
|
||||
cursorScrollMargin: 10,
|
||||
lineWrapping: true,
|
||||
dragDrop: false,
|
||||
extraKeys: {
|
||||
Home: 'goLineLeft',
|
||||
End: 'goLineRight'
|
||||
}
|
||||
});
|
||||
|
||||
// Markdown shortcuts for the editor
|
||||
_.each(MarkdownShortcuts, function (combo) {
|
||||
shortcut.add(combo.key, function () {
|
||||
return codemirror.addMarkdown({style: combo.style});
|
||||
});
|
||||
});
|
||||
|
||||
// Public API
|
||||
_.extend(this, {
|
||||
codemirror: codemirror,
|
||||
|
||||
scrollViewPort: function () {
|
||||
return $('.CodeMirror-scroll');
|
||||
},
|
||||
scrollContent: function () {
|
||||
return $('.CodeMirror-sizer');
|
||||
},
|
||||
enable: function () {
|
||||
codemirror.setOption('readOnly', false);
|
||||
codemirror.on('change', function () {
|
||||
$(document).trigger('markdownEditorChange');
|
||||
});
|
||||
},
|
||||
disable: function () {
|
||||
codemirror.setOption('readOnly', 'nocursor');
|
||||
codemirror.off('change', function () {
|
||||
$(document).trigger('markdownEditorChange');
|
||||
});
|
||||
},
|
||||
isCursorAtEnd: function () {
|
||||
return codemirror.getCursor('end').line > codemirror.lineCount() - 5;
|
||||
},
|
||||
value: function () {
|
||||
return codemirror.getValue();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
Ghost.Editor = Ghost.Editor || {};
|
||||
Ghost.Editor.MarkdownEditor = MarkdownEditor;
|
||||
} ());
|
154
core/clientold/assets/lib/editor/markerManager.js
Normal file
154
core/clientold/assets/lib/editor/markerManager.js
Normal file
@ -0,0 +1,154 @@
|
||||
// # Ghost Editor Marker Manager
|
||||
//
|
||||
// MarkerManager looks after the array of markers which are attached to image markdown in the editor.
|
||||
//
|
||||
// Marker Manager is told by the Upload Manager to add a marker to a line.
|
||||
// A marker takes the form of a 'magic id' which looks like:
|
||||
// {<1>}
|
||||
// It is appended to the start of the given line, and then defined as a CodeMirror 'TextMarker' widget which is
|
||||
// subsequently added to an array of markers to keep track of all markers in the editor.
|
||||
// The TextMarker is also set to 'collapsed' mode which means it does not show up in the display.
|
||||
// Currently, the markers can be seen if you copy and paste your content out of Ghost into a text editor.
|
||||
// The markers are stripped on save so should not appear in the DB
|
||||
|
||||
|
||||
/*global _, Ghost */
|
||||
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
var imageMarkdownRegex = /^(?:\{<(.*?)>\})?!(?:\[([^\n\]]*)\])(?:\(([^\n\]]*)\))?$/gim,
|
||||
markerRegex = /\{<([\w\W]*?)>\}/,
|
||||
MarkerManager;
|
||||
|
||||
MarkerManager = function (editor) {
|
||||
var markers = {},
|
||||
uploadPrefix = 'image_upload',
|
||||
uploadId = 1,
|
||||
addMarker,
|
||||
removeMarker,
|
||||
markerRegexForId,
|
||||
stripMarkerFromLine,
|
||||
findAndStripMarker,
|
||||
checkMarkers,
|
||||
initMarkers;
|
||||
|
||||
// the regex
|
||||
markerRegexForId = function (id) {
|
||||
id = id.replace('image_upload_', '');
|
||||
return new RegExp('\\{<' + id + '>\\}', 'gmi');
|
||||
};
|
||||
|
||||
// Add a marker to the given line
|
||||
// Params:
|
||||
// line - CodeMirror LineHandle
|
||||
// ln - line number
|
||||
addMarker = function (line, ln) {
|
||||
var marker,
|
||||
magicId = '{<' + uploadId + '>}',
|
||||
newText = magicId + line.text;
|
||||
|
||||
editor.replaceRange(
|
||||
newText,
|
||||
{line: ln, ch: 0},
|
||||
{line: ln, ch: newText.length}
|
||||
);
|
||||
|
||||
marker = editor.markText(
|
||||
{line: ln, ch: 0},
|
||||
{line: ln, ch: (magicId.length)},
|
||||
{collapsed: true}
|
||||
);
|
||||
|
||||
markers[uploadPrefix + '_' + uploadId] = marker;
|
||||
uploadId += 1;
|
||||
};
|
||||
|
||||
// Remove a marker
|
||||
// Will be passed a LineHandle if we already know which line the marker is on
|
||||
removeMarker = function (id, marker, line) {
|
||||
delete markers[id];
|
||||
marker.clear();
|
||||
|
||||
if (line) {
|
||||
stripMarkerFromLine(line);
|
||||
} else {
|
||||
findAndStripMarker(id);
|
||||
}
|
||||
};
|
||||
|
||||
// Removes the marker on the given line if there is one
|
||||
stripMarkerFromLine = function (line) {
|
||||
var markerText = line.text.match(markerRegex),
|
||||
ln = editor.getLineNumber(line);
|
||||
|
||||
if (markerText) {
|
||||
editor.replaceRange(
|
||||
'',
|
||||
{line: ln, ch: markerText.index},
|
||||
{line: ln, ch: markerText.index + markerText[0].length}
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
// Find a marker in the editor by id & remove it
|
||||
// Goes line by line to find the marker by it's text if we've lost track of the TextMarker
|
||||
findAndStripMarker = function (id) {
|
||||
editor.eachLine(function (line) {
|
||||
var markerText = markerRegexForId(id).exec(line.text),
|
||||
ln;
|
||||
|
||||
if (markerText) {
|
||||
ln = editor.getLineNumber(line);
|
||||
editor.replaceRange(
|
||||
'',
|
||||
{line: ln, ch: markerText.index},
|
||||
{line: ln, ch: markerText.index + markerText[0].length}
|
||||
);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// Check each marker to see if it is still present in the editor and if it still corresponds to image markdown
|
||||
// If it is no longer a valid image, remove it
|
||||
checkMarkers = function () {
|
||||
_.each(markers, function (marker, id) {
|
||||
var line;
|
||||
marker = markers[id];
|
||||
if (marker.find()) {
|
||||
line = editor.getLineHandle(marker.find().from.line);
|
||||
if (!line.text.match(imageMarkdownRegex)) {
|
||||
removeMarker(id, marker, line);
|
||||
}
|
||||
} else {
|
||||
removeMarker(id, marker);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// Add markers to the line if it needs one
|
||||
initMarkers = function (line) {
|
||||
var isImage = line.text.match(imageMarkdownRegex),
|
||||
hasMarker = line.text.match(markerRegex);
|
||||
|
||||
if (isImage && !hasMarker) {
|
||||
addMarker(line, editor.getLineNumber(line));
|
||||
}
|
||||
};
|
||||
|
||||
// Initialise
|
||||
editor.eachLine(initMarkers);
|
||||
|
||||
// Public API
|
||||
_.extend(this, {
|
||||
markers: markers,
|
||||
checkMarkers: checkMarkers,
|
||||
addMarker: addMarker,
|
||||
stripMarkerFromLine: stripMarkerFromLine,
|
||||
getMarkerRegexForId: markerRegexForId
|
||||
});
|
||||
};
|
||||
|
||||
Ghost.Editor = Ghost.Editor || {};
|
||||
Ghost.Editor.MarkerManager = MarkerManager;
|
||||
}());
|
112
core/clientold/assets/lib/editor/mobileCodeMirror.js
Normal file
112
core/clientold/assets/lib/editor/mobileCodeMirror.js
Normal file
@ -0,0 +1,112 @@
|
||||
// Taken from js-bin with thanks to Remy Sharp
|
||||
// yeah, nasty, but it allows me to switch from a RTF to plain text if we're running a iOS
|
||||
|
||||
/*global Ghost, $, _, DocumentTouch, CodeMirror*/
|
||||
(function () {
|
||||
Ghost.touchEditor = false;
|
||||
|
||||
var noop = function () {},
|
||||
hasTouchScreen,
|
||||
smallScreen,
|
||||
TouchEditor,
|
||||
_oldCM,
|
||||
key;
|
||||
|
||||
// Taken from "Responsive design & the Guardian" with thanks to Matt Andrews
|
||||
// Added !window._phantom so that the functional tests run as though this is not a touch screen.
|
||||
// In future we can do something more advanced here for testing both touch and non touch
|
||||
hasTouchScreen = function () {
|
||||
return !window._phantom &&
|
||||
(
|
||||
('ontouchstart' in window) ||
|
||||
(window.DocumentTouch && document instanceof DocumentTouch)
|
||||
);
|
||||
};
|
||||
|
||||
smallScreen = function () {
|
||||
if (window.matchMedia('(max-width: 1000px)').matches) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
if (hasTouchScreen()) {
|
||||
$('body').addClass('touch-editor');
|
||||
Ghost.touchEditor = true;
|
||||
|
||||
TouchEditor = function (el, options) {
|
||||
/*jshint unused:false*/
|
||||
this.textarea = el;
|
||||
this.win = { document : this.textarea };
|
||||
this.ready = true;
|
||||
this.wrapping = document.createElement('div');
|
||||
|
||||
var textareaParent = this.textarea.parentNode;
|
||||
this.wrapping.appendChild(this.textarea);
|
||||
textareaParent.appendChild(this.wrapping);
|
||||
|
||||
this.textarea.style.opacity = 1;
|
||||
|
||||
$(this.textarea).blur(_.throttle(function () {
|
||||
$(document).trigger('markdownEditorChange', { panelId: el.id });
|
||||
}, 200));
|
||||
|
||||
if (!smallScreen()) {
|
||||
$(this.textarea).on('change', _.throttle(function () {
|
||||
$(document).trigger('markdownEditorChange', { panelId: el.id });
|
||||
}, 200));
|
||||
}
|
||||
};
|
||||
|
||||
TouchEditor.prototype = {
|
||||
setOption: function (type, handler) {
|
||||
if (type === 'onChange') {
|
||||
$(this.textarea).change(handler);
|
||||
}
|
||||
},
|
||||
eachLine: function () {
|
||||
return [];
|
||||
},
|
||||
getValue: function () {
|
||||
return this.textarea.value;
|
||||
},
|
||||
setValue: function (code) {
|
||||
this.textarea.value = code;
|
||||
},
|
||||
focus: noop,
|
||||
getCursor: function () {
|
||||
return { line: 0, ch: 0 };
|
||||
},
|
||||
setCursor: noop,
|
||||
currentLine: function () {
|
||||
return 0;
|
||||
},
|
||||
cursorPosition: function () {
|
||||
return { character: 0 };
|
||||
},
|
||||
addMarkdown: noop,
|
||||
nthLine: noop,
|
||||
refresh: noop,
|
||||
selectLines: noop,
|
||||
on: noop
|
||||
};
|
||||
|
||||
_oldCM = CodeMirror;
|
||||
|
||||
// CodeMirror = noop;
|
||||
|
||||
for (key in _oldCM) {
|
||||
if (_oldCM.hasOwnProperty(key)) {
|
||||
CodeMirror[key] = noop;
|
||||
}
|
||||
}
|
||||
|
||||
CodeMirror.fromTextArea = function (el, options) {
|
||||
return new TouchEditor(el, options);
|
||||
};
|
||||
|
||||
CodeMirror.keyMap = { basic: {} };
|
||||
|
||||
}
|
||||
}());
|
47
core/clientold/assets/lib/editor/scrollHandler.js
Normal file
47
core/clientold/assets/lib/editor/scrollHandler.js
Normal file
@ -0,0 +1,47 @@
|
||||
// # Ghost Editor Scroll Handler
|
||||
//
|
||||
// Scroll Handler does the (currently very simple / naive) job of syncing the right pane with the left pane
|
||||
// as the right pane scrolls
|
||||
|
||||
/*global Ghost, _ */
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
var ScrollHandler = function (markdown, preview) {
|
||||
var $markdownViewPort = markdown.scrollViewPort(),
|
||||
$previewViewPort = preview.scrollViewPort(),
|
||||
$markdownContent = markdown.scrollContent(),
|
||||
$previewContent = preview.scrollContent(),
|
||||
syncScroll;
|
||||
|
||||
syncScroll = _.throttle(function () {
|
||||
// calc position
|
||||
var markdownHeight = $markdownContent.height() - $markdownViewPort.height(),
|
||||
previewHeight = $previewContent.height() - $previewViewPort.height(),
|
||||
ratio = previewHeight / markdownHeight,
|
||||
previewPosition = $markdownViewPort.scrollTop() * ratio;
|
||||
|
||||
if (markdown.isCursorAtEnd()) {
|
||||
previewPosition = previewHeight + 30;
|
||||
}
|
||||
|
||||
// apply new scroll
|
||||
$previewViewPort.scrollTop(previewPosition);
|
||||
}, 10);
|
||||
|
||||
_.extend(this, {
|
||||
enable: function () { // Handle Scroll Events
|
||||
$markdownViewPort.on('scroll', syncScroll);
|
||||
$markdownViewPort.scrollClass({target: '.entry-markdown', offset: 10});
|
||||
$previewViewPort.scrollClass({target: '.entry-preview', offset: 10});
|
||||
},
|
||||
disable: function () {
|
||||
$markdownViewPort.off('scroll', syncScroll);
|
||||
}
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
Ghost.Editor = Ghost.Editor || {};
|
||||
Ghost.Editor.ScrollHandler = ScrollHandler;
|
||||
} ());
|
153
core/clientold/assets/lib/editor/uploadManager.js
Normal file
153
core/clientold/assets/lib/editor/uploadManager.js
Normal file
@ -0,0 +1,153 @@
|
||||
// # Ghost Editor Upload Manager
|
||||
//
|
||||
// UploadManager ensures that markdown gets updated when images get uploaded via the Preview.
|
||||
//
|
||||
// The Ghost Editor has a particularly tricky problem to solve, in that it is possible to upload an image by
|
||||
// interacting with the preview. The process of uploading an image is handled by uploader.js, but there is still
|
||||
// a lot of work needed to ensure that uploaded files end up in the right place - that is that the image
|
||||
// path gets added to the correct piece of markdown in the editor.
|
||||
//
|
||||
// To solve this, Ghost adds a unique 'marker' to each piece of markdown which represents an image:
|
||||
// More detail about how the markers work can be find in markerManager.js
|
||||
//
|
||||
// UploadManager handles changes in the editor, looking for text which matches image markdown, and telling the marker
|
||||
// manager to add a marker. It also checks changed lines to see if they have a marker but are no longer an image.
|
||||
//
|
||||
// UploadManager's most important job is handling uploads such that when a successful upload completes, the correct
|
||||
// piece of image markdown is updated with the path.
|
||||
// This is done in part by ghostImagePreview.js, which takes the marker from the markdown and uses it to create an ID
|
||||
// on the dropzone. When an upload completes successfully from uploader.js, the event thrown contains reference to the
|
||||
// dropzone, from which uploadManager can pull the ID & then get the right marker from the Marker Manager.
|
||||
//
|
||||
// Without a doubt, the separation of concerns between the uploadManager, and the markerManager could be vastly
|
||||
// improved
|
||||
|
||||
|
||||
/*global $, _, Ghost */
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
var imageMarkdownRegex = /^(?:\{<(.*?)>\})?!(?:\[([^\n\]]*)\])(?:\(([^\n\]]*)\))?$/gim,
|
||||
markerRegex = /\{<([\w\W]*?)>\}/,
|
||||
UploadManager;
|
||||
|
||||
UploadManager = function (markdown) {
|
||||
var editor = markdown.codemirror,
|
||||
markerMgr = new Ghost.Editor.MarkerManager(editor),
|
||||
findLine,
|
||||
checkLine,
|
||||
value,
|
||||
handleUpload,
|
||||
handleChange;
|
||||
|
||||
// Find the line with the marker which matches
|
||||
findLine = function (result_id) {
|
||||
// try to find the right line to replace
|
||||
if (markerMgr.markers.hasOwnProperty(result_id) && markerMgr.markers[result_id].find()) {
|
||||
return editor.getLineHandle(markerMgr.markers[result_id].find().from.line);
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
// Check the given line to see if it has an image, and if it correctly has a marker
|
||||
// In the special case of lines which were just pasted in, any markers are removed to prevent duplication
|
||||
checkLine = function (ln, mode) {
|
||||
var line = editor.getLineHandle(ln),
|
||||
isImage = line.text.match(imageMarkdownRegex),
|
||||
hasMarker;
|
||||
|
||||
// We care if it is an image
|
||||
if (isImage) {
|
||||
hasMarker = line.text.match(markerRegex);
|
||||
|
||||
if (hasMarker && (mode === 'paste' || mode === 'undo')) {
|
||||
// this could be a duplicate, and won't be a real marker
|
||||
markerMgr.stripMarkerFromLine(line);
|
||||
}
|
||||
|
||||
if (!hasMarker) {
|
||||
markerMgr.addMarker(line, ln);
|
||||
}
|
||||
}
|
||||
// TODO: hasMarker but no image?
|
||||
};
|
||||
|
||||
// Get the markdown with all the markers stripped
|
||||
value = function () {
|
||||
var value = editor.getValue();
|
||||
|
||||
_.each(markerMgr.markers, function (marker, id) {
|
||||
/*jshint unused:false*/
|
||||
value = value.replace(markerMgr.getMarkerRegexForId(id), '');
|
||||
});
|
||||
|
||||
return value;
|
||||
};
|
||||
|
||||
// Match the uploaded file to a line in the editor, and update that line with a path reference
|
||||
// ensuring that everything ends up in the correct place and format.
|
||||
handleUpload = function (e, result_src) {
|
||||
var line = findLine($(e.currentTarget).attr('id')),
|
||||
lineNumber = editor.getLineNumber(line),
|
||||
match = line.text.match(/\([^\n]*\)?/),
|
||||
replacement = '(http://)';
|
||||
|
||||
if (match) {
|
||||
// simple case, we have the parenthesis
|
||||
editor.setSelection(
|
||||
{line: lineNumber, ch: match.index + 1},
|
||||
{line: lineNumber, ch: match.index + match[0].length - 1}
|
||||
);
|
||||
} else {
|
||||
match = line.text.match(/\]/);
|
||||
if (match) {
|
||||
editor.replaceRange(
|
||||
replacement,
|
||||
{line: lineNumber, ch: match.index + 1},
|
||||
{line: lineNumber, ch: match.index + 1}
|
||||
);
|
||||
editor.setSelection(
|
||||
{line: lineNumber, ch: match.index + 2},
|
||||
{line: lineNumber, ch: match.index + replacement.length }
|
||||
);
|
||||
}
|
||||
}
|
||||
editor.replaceSelection(result_src);
|
||||
};
|
||||
|
||||
// Change events from CodeMirror tell us which lines have changed.
|
||||
// Each changed line is then checked to see if a marker needs to be added or removed
|
||||
handleChange = function (cm, changeObj) {
|
||||
/*jshint unused:false*/
|
||||
var linesChanged = _.range(changeObj.from.line, changeObj.from.line + changeObj.text.length);
|
||||
|
||||
_.each(linesChanged, function (ln) {
|
||||
checkLine(ln, changeObj.origin);
|
||||
});
|
||||
|
||||
// Is this a line which may have had a marker on it?
|
||||
markerMgr.checkMarkers();
|
||||
};
|
||||
|
||||
// Public API
|
||||
_.extend(this, {
|
||||
value: value,
|
||||
enable: function () {
|
||||
var filestorage = $('#entry-markdown-content').data('filestorage');
|
||||
$('.js-drop-zone').upload({editor: true, fileStorage: filestorage});
|
||||
$('.js-drop-zone').on('uploadstart', markdown.off);
|
||||
$('.js-drop-zone').on('uploadfailure', markdown.on);
|
||||
$('.js-drop-zone').on('uploadsuccess', markdown.on);
|
||||
$('.js-drop-zone').on('uploadsuccess', handleUpload);
|
||||
},
|
||||
disable: function () {
|
||||
$('.js-drop-zone').off('uploadsuccess', handleUpload);
|
||||
}
|
||||
});
|
||||
|
||||
editor.on('change', handleChange);
|
||||
};
|
||||
Ghost.Editor = Ghost.Editor || {};
|
||||
Ghost.Editor.UploadManager = UploadManager;
|
||||
}());
|
@ -130,13 +130,13 @@
|
||||
CodeMirror.prototype.addMarkdown.options = {
|
||||
style: null,
|
||||
syntax: {
|
||||
bold: "**$1**",
|
||||
italic: "*$1*",
|
||||
strike: "~~$1~~",
|
||||
code: "`$1`",
|
||||
link: "[$1](http://)",
|
||||
image: "![$1](http://)",
|
||||
blockquote: "> $1"
|
||||
bold: '**$1**',
|
||||
italic: '*$1*',
|
||||
strike: '~~$1~~',
|
||||
code: '`$1`',
|
||||
link: '[$1](http://)',
|
||||
image: '![$1](http://)',
|
||||
blockquote: '> $1'
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -316,7 +316,7 @@
|
||||
},
|
||||
afterRender: function () {
|
||||
this.$el.fadeIn(50);
|
||||
$(".modal-background").fadeIn(10, function () {
|
||||
$(".modal-background").show(10, function () {
|
||||
$(this).addClass("in");
|
||||
});
|
||||
if (this.model.options.confirm) {
|
||||
|
@ -228,6 +228,7 @@
|
||||
},
|
||||
|
||||
toggleFeatured: function (e) {
|
||||
e.preventDefault();
|
||||
var self = this,
|
||||
featured = !self.model.get('featured'),
|
||||
featuredEl = $(e.currentTarget),
|
||||
@ -238,6 +239,7 @@
|
||||
}, {
|
||||
success : function () {
|
||||
featuredEl.removeClass("featured unfeatured").addClass(featured ? "featured" : "unfeatured");
|
||||
Ghost.notifications.clearEverything();
|
||||
Ghost.notifications.addItem({
|
||||
type: 'success',
|
||||
message: "Post successfully marked as " + (featured ? "featured" : "not featured") + ".",
|
||||
|
255
core/clientold/views/editor-actions-widget.js
Normal file
255
core/clientold/views/editor-actions-widget.js
Normal file
@ -0,0 +1,255 @@
|
||||
// The Save / Publish button
|
||||
|
||||
/*global $, _, Ghost, shortcut */
|
||||
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
// The Publish, Queue, Publish Now buttons
|
||||
// ----------------------------------------
|
||||
Ghost.View.EditorActionsWidget = Ghost.View.extend({
|
||||
|
||||
events: {
|
||||
'click [data-set-status]': 'handleStatus',
|
||||
'click .js-publish-button': 'handlePostButton'
|
||||
},
|
||||
|
||||
statusMap: null,
|
||||
|
||||
createStatusMap: {
|
||||
'draft': 'Save Draft',
|
||||
'published': 'Publish Now'
|
||||
},
|
||||
|
||||
updateStatusMap: {
|
||||
'draft': 'Unpublish',
|
||||
'published': 'Update Post'
|
||||
},
|
||||
|
||||
//TODO: This has to be moved to the I18n localization file.
|
||||
//This structure is supposed to be close to the i18n-localization which will be used soon.
|
||||
messageMap: {
|
||||
errors: {
|
||||
post: {
|
||||
published: {
|
||||
'published': 'Your post could not be updated.',
|
||||
'draft': 'Your post could not be saved as a draft.'
|
||||
},
|
||||
draft: {
|
||||
'published': 'Your post could not be published.',
|
||||
'draft': 'Your post could not be saved as a draft.'
|
||||
}
|
||||
|
||||
}
|
||||
},
|
||||
|
||||
success: {
|
||||
post: {
|
||||
published: {
|
||||
'published': 'Your post has been updated.',
|
||||
'draft': 'Your post has been saved as a draft.'
|
||||
},
|
||||
draft: {
|
||||
'published': 'Your post has been published.',
|
||||
'draft': 'Your post has been saved as a draft.'
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
initialize: function () {
|
||||
var self = this;
|
||||
|
||||
// Toggle publish
|
||||
shortcut.add('Ctrl+Alt+P', function () {
|
||||
self.toggleStatus();
|
||||
});
|
||||
shortcut.add('Ctrl+S', function () {
|
||||
self.updatePost();
|
||||
});
|
||||
shortcut.add('Meta+S', function () {
|
||||
self.updatePost();
|
||||
});
|
||||
this.listenTo(this.model, 'change:status', this.render);
|
||||
},
|
||||
|
||||
toggleStatus: function () {
|
||||
var self = this,
|
||||
keys = Object.keys(this.statusMap),
|
||||
model = self.model,
|
||||
prevStatus = model.get('status'),
|
||||
currentIndex = keys.indexOf(prevStatus),
|
||||
newIndex,
|
||||
status;
|
||||
|
||||
newIndex = currentIndex + 1 > keys.length - 1 ? 0 : currentIndex + 1;
|
||||
status = keys[newIndex];
|
||||
|
||||
this.setActiveStatus(keys[newIndex], this.statusMap[status], prevStatus);
|
||||
|
||||
this.savePost({
|
||||
status: keys[newIndex]
|
||||
}).then(function () {
|
||||
self.reportSaveSuccess(status, prevStatus);
|
||||
}, function (xhr) {
|
||||
// Show a notification about the error
|
||||
self.reportSaveError(xhr, model, status, prevStatus);
|
||||
});
|
||||
},
|
||||
|
||||
setActiveStatus: function (newStatus, displayText, currentStatus) {
|
||||
var isPublishing = (newStatus === 'published' && currentStatus !== 'published'),
|
||||
isUnpublishing = (newStatus === 'draft' && currentStatus === 'published'),
|
||||
// Controls when background of button has the splitbutton-delete/button-delete classes applied
|
||||
isImportantStatus = (isPublishing || isUnpublishing);
|
||||
|
||||
$('.js-publish-splitbutton')
|
||||
.removeClass(isImportantStatus ? 'splitbutton-save' : 'splitbutton-delete')
|
||||
.addClass(isImportantStatus ? 'splitbutton-delete' : 'splitbutton-save');
|
||||
|
||||
// Set the publish button's action and proper coloring
|
||||
$('.js-publish-button')
|
||||
.attr('data-status', newStatus)
|
||||
.text(displayText)
|
||||
.removeClass(isImportantStatus ? 'button-save' : 'button-delete')
|
||||
.addClass(isImportantStatus ? 'button-delete' : 'button-save');
|
||||
|
||||
// Remove the animated popup arrow
|
||||
$('.js-publish-splitbutton > a')
|
||||
.removeClass('active');
|
||||
|
||||
// Set the active action in the popup
|
||||
$('.js-publish-splitbutton .editor-options li')
|
||||
.removeClass('active')
|
||||
.filter(['li[data-set-status="', newStatus, '"]'].join(''))
|
||||
.addClass('active');
|
||||
},
|
||||
|
||||
handleStatus: function (e) {
|
||||
if (e) { e.preventDefault(); }
|
||||
var status = $(e.currentTarget).attr('data-set-status'),
|
||||
currentStatus = this.model.get('status');
|
||||
|
||||
this.setActiveStatus(status, this.statusMap[status], currentStatus);
|
||||
|
||||
// Dismiss the popup menu
|
||||
$('body').find('.overlay:visible').fadeOut();
|
||||
},
|
||||
|
||||
handlePostButton: function (e) {
|
||||
if (e) { e.preventDefault(); }
|
||||
var status = $(e.currentTarget).attr('data-status');
|
||||
|
||||
this.updatePost(status);
|
||||
},
|
||||
|
||||
updatePost: function (status) {
|
||||
var self = this,
|
||||
model = this.model,
|
||||
prevStatus = model.get('status');
|
||||
|
||||
// Default to same status if not passed in
|
||||
status = status || prevStatus;
|
||||
|
||||
model.trigger('willSave');
|
||||
|
||||
this.savePost({
|
||||
status: status
|
||||
}).then(function () {
|
||||
self.reportSaveSuccess(status, prevStatus);
|
||||
// Refresh publish button and all relevant controls with updated status.
|
||||
self.render();
|
||||
}, function (xhr) {
|
||||
// Set the model status back to previous
|
||||
model.set({ status: prevStatus });
|
||||
// Set appropriate button status
|
||||
self.setActiveStatus(status, self.statusMap[status], prevStatus);
|
||||
// Show a notification about the error
|
||||
self.reportSaveError(xhr, model, status, prevStatus);
|
||||
});
|
||||
},
|
||||
|
||||
savePost: function (data) {
|
||||
var publishButton = $('.js-publish-button'),
|
||||
saved,
|
||||
enablePublish = function (deferred) {
|
||||
deferred.always(function () {
|
||||
publishButton.prop('disabled', false);
|
||||
});
|
||||
return deferred;
|
||||
};
|
||||
|
||||
publishButton.prop('disabled', true);
|
||||
|
||||
_.each(this.model.blacklist, function (item) {
|
||||
this.model.unset(item);
|
||||
}, this);
|
||||
|
||||
saved = this.model.save(_.extend({
|
||||
title: this.options.$title.val(),
|
||||
markdown: this.options.editor.value()
|
||||
}, data));
|
||||
|
||||
// TODO: Take this out if #2489 gets merged in Backbone. Or patch Backbone
|
||||
// ourselves for more consistent promises.
|
||||
if (saved) {
|
||||
return enablePublish(saved);
|
||||
}
|
||||
|
||||
return enablePublish($.Deferred().reject());
|
||||
},
|
||||
|
||||
reportSaveSuccess: function (status, prevStatus) {
|
||||
Ghost.notifications.clearEverything();
|
||||
Ghost.notifications.addItem({
|
||||
type: 'success',
|
||||
message: this.messageMap.success.post[prevStatus][status],
|
||||
status: 'passive'
|
||||
});
|
||||
this.options.editor.setDirty(false);
|
||||
},
|
||||
|
||||
reportSaveError: function (response, model, status, prevStatus) {
|
||||
var message = this.messageMap.errors.post[prevStatus][status];
|
||||
|
||||
if (response) {
|
||||
// Get message from response
|
||||
message += ' ' + Ghost.Views.Utils.getRequestErrorMessage(response);
|
||||
} else if (model.validationError) {
|
||||
// Grab a validation error
|
||||
message += ' ' + model.validationError;
|
||||
}
|
||||
|
||||
Ghost.notifications.clearEverything();
|
||||
Ghost.notifications.addItem({
|
||||
type: 'error',
|
||||
message: message,
|
||||
status: 'passive'
|
||||
});
|
||||
},
|
||||
|
||||
setStatusLabels: function (statusMap) {
|
||||
_.each(statusMap, function (label, status) {
|
||||
$('li[data-set-status="' + status + '"] > a').text(label);
|
||||
});
|
||||
},
|
||||
|
||||
render: function () {
|
||||
var status = this.model.get('status');
|
||||
|
||||
// Assume that we're creating a new post
|
||||
if (status !== 'published') {
|
||||
this.statusMap = this.createStatusMap;
|
||||
} else {
|
||||
this.statusMap = this.updateStatusMap;
|
||||
}
|
||||
|
||||
// Populate the publish menu with the appropriate verbiage
|
||||
this.setStatusLabels(this.statusMap);
|
||||
|
||||
// Default the selected publish option to the current status of the post.
|
||||
this.setActiveStatus(status, this.statusMap[status], status);
|
||||
}
|
||||
|
||||
});
|
||||
}());
|
@ -45,7 +45,7 @@
|
||||
|
||||
if (tags) {
|
||||
_.forEach(tags, function (tag) {
|
||||
var $tag = $('<span class="tag" data-tag-id="' + tag.id + '">' + tag.name + '</span>');
|
||||
var $tag = $('<span class="tag" data-tag-id="' + tag.id + '">' + _.escape(tag.name) + '</span>');
|
||||
$tags.append($tag);
|
||||
$("[data-tag-id=" + tag.id + "]")[0].scrollIntoView(true);
|
||||
});
|
||||
@ -106,8 +106,11 @@
|
||||
styles = {
|
||||
left: $target.position().left
|
||||
},
|
||||
maxSuggestions = 5, // Limit the suggestions number
|
||||
regexTerm = searchTerm.replace(/(\s+)/g, "(<[^>]+>)*$1(<[^>]+>)*"),
|
||||
// Limit the suggestions number
|
||||
maxSuggestions = 5,
|
||||
// Escape regex special characters
|
||||
escapedTerm = searchTerm.replace(/[\-\/\\\^$*+?.()|\[\]{}]/g, '\\$&'),
|
||||
regexTerm = escapedTerm.replace(/(\s+)/g, "(<[^>]+>)*$1(<[^>]+>)*"),
|
||||
regexPattern = new RegExp("(" + regexTerm + ")", "i");
|
||||
|
||||
this.$suggestions.css(styles);
|
||||
@ -121,10 +124,14 @@
|
||||
var highlightedName,
|
||||
suggestionHTML;
|
||||
|
||||
highlightedName = matchingTag.name.replace(regexPattern, "<mark>$1</mark>");
|
||||
highlightedName = matchingTag.name.replace(regexPattern, function (match, p1) {
|
||||
return "<mark>" + _.escape(p1) + "</mark>";
|
||||
});
|
||||
/*jslint regexp: true */ // - would like to remove this
|
||||
highlightedName = highlightedName.replace(/(<mark>[^<>]*)((<[^>]+>)+)([^<>]*<\/mark>)/, "$1</mark>$2<mark>$4");
|
||||
|
||||
highlightedName = highlightedName.replace(/(<mark>[^<>]*)((<[^>]+>)+)([^<>]*<\/mark>)/, function (match, p1, p2, p3, p4) {
|
||||
return _.escape(p1) + '</mark>' + _.escape(p2) + '<mark>' + _.escape(p4);
|
||||
});
|
||||
|
||||
suggestionHTML = "<li data-tag-id='" + matchingTag.id + "' data-tag-name='" + _.escape(matchingTag.name) + "'><a href='#'>" + highlightedName + "</a></li>";
|
||||
this.$suggestions.append(suggestionHTML);
|
||||
}, this);
|
||||
@ -185,7 +192,8 @@
|
||||
searchTerm = $.trim($target.val()),
|
||||
tag,
|
||||
$selectedSuggestion,
|
||||
isComma = ",".localeCompare(String.fromCharCode(e.keyCode || e.charCode)) === 0;
|
||||
isComma = ",".localeCompare(String.fromCharCode(e.keyCode || e.charCode)) === 0,
|
||||
hasAlreadyBeenAdded;
|
||||
|
||||
// use localeCompare in case of international keyboard layout
|
||||
if ((e.keyCode === this.keys.ENTER || isComma) && searchTerm) {
|
||||
@ -194,16 +202,19 @@
|
||||
|
||||
$selectedSuggestion = this.$suggestions.children(".selected");
|
||||
if (this.$suggestions.is(":visible") && $selectedSuggestion.length !== 0) {
|
||||
|
||||
if ($('.tag:containsExact("' + _.unescape($selectedSuggestion.data('tag-name')) + '")').length === 0) {
|
||||
tag = {id: $selectedSuggestion.data('tag-id'), name: _.unescape($selectedSuggestion.data('tag-name'))};
|
||||
tag = {id: $selectedSuggestion.data('tag-id'), name: _.unescape($selectedSuggestion.data('tag-name'))};
|
||||
hasAlreadyBeenAdded = this.hasTagBeenAdded(tag.name);
|
||||
if (!hasAlreadyBeenAdded) {
|
||||
this.addTag(tag);
|
||||
}
|
||||
} else {
|
||||
if (isComma) {
|
||||
// Remove comma from string if comma is used to submit.
|
||||
searchTerm = searchTerm.replace(/,/g, "");
|
||||
} // Remove comma from string if comma is used to submit.
|
||||
if ($('.tag:containsExact("' + searchTerm + '")').length === 0) {
|
||||
}
|
||||
|
||||
hasAlreadyBeenAdded = this.hasTagBeenAdded(searchTerm);
|
||||
if (!hasAlreadyBeenAdded) {
|
||||
this.addTag({id: null, name: searchTerm});
|
||||
}
|
||||
}
|
||||
@ -216,13 +227,9 @@
|
||||
completeCurrentTag: function () {
|
||||
var $target = this.$('.tag-input'),
|
||||
tagName = $target.val(),
|
||||
usedTagNames,
|
||||
hasAlreadyBeenAdded;
|
||||
|
||||
usedTagNames = _.map(this.model.get('tags'), function (tag) {
|
||||
return tag.name.toUpperCase();
|
||||
});
|
||||
hasAlreadyBeenAdded = usedTagNames.indexOf(tagName.toUpperCase()) !== -1;
|
||||
hasAlreadyBeenAdded = this.hasTagBeenAdded(tagName);
|
||||
|
||||
if (tagName.length > 0 && !hasAlreadyBeenAdded) {
|
||||
this.addTag({id: null, name: tagName});
|
||||
@ -267,9 +274,8 @@
|
||||
|
||||
tagNameMatches = tag.name.toUpperCase().indexOf(searchTerm) !== -1;
|
||||
|
||||
hasAlreadyBeenAdded = _.some(self.model.get('tags'), function (usedTag) {
|
||||
return tag.name.toUpperCase() === usedTag.name.toUpperCase();
|
||||
});
|
||||
hasAlreadyBeenAdded = self.hasTagBeenAdded(tag.name);
|
||||
|
||||
return tagNameMatches && !hasAlreadyBeenAdded;
|
||||
});
|
||||
|
||||
@ -277,7 +283,7 @@
|
||||
},
|
||||
|
||||
addTag: function (tag) {
|
||||
var $tag = $('<span class="tag" data-tag-id="' + tag.id + '">' + tag.name + '</span>');
|
||||
var $tag = $('<span class="tag" data-tag-id="' + tag.id + '">' + _.escape(tag.name) + '</span>');
|
||||
this.$('.tags').append($tag);
|
||||
$(".tag").last()[0].scrollIntoView(true);
|
||||
window.scrollTo(0, 1);
|
||||
@ -285,6 +291,12 @@
|
||||
|
||||
this.$('.tag-input').val('').focus();
|
||||
this.$suggestions.hide();
|
||||
},
|
||||
|
||||
hasTagBeenAdded: function (tagName) {
|
||||
return _.some(this.model.get('tags'), function (usedTag) {
|
||||
return tagName.toUpperCase() === usedTag.name.toUpperCase();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -1,45 +1,10 @@
|
||||
// # Article Editor
|
||||
|
||||
/*global window, document, setTimeout, navigator, $, _, Backbone, Ghost, Showdown, CodeMirror, shortcut, Countable */
|
||||
/*global document, setTimeout, navigator, $, Backbone, Ghost, shortcut */
|
||||
(function () {
|
||||
"use strict";
|
||||
'use strict';
|
||||
|
||||
/*jslint regexp: true, bitwise: true */
|
||||
var PublishBar,
|
||||
ActionsWidget,
|
||||
UploadManager,
|
||||
MarkerManager,
|
||||
MarkdownShortcuts = [
|
||||
{'key': 'Ctrl+B', 'style': 'bold'},
|
||||
{'key': 'Meta+B', 'style': 'bold'},
|
||||
{'key': 'Ctrl+I', 'style': 'italic'},
|
||||
{'key': 'Meta+I', 'style': 'italic'},
|
||||
{'key': 'Ctrl+Alt+U', 'style': 'strike'},
|
||||
{'key': 'Ctrl+Shift+K', 'style': 'code'},
|
||||
{'key': 'Meta+K', 'style': 'code'},
|
||||
{'key': 'Ctrl+Alt+1', 'style': 'h1'},
|
||||
{'key': 'Ctrl+Alt+2', 'style': 'h2'},
|
||||
{'key': 'Ctrl+Alt+3', 'style': 'h3'},
|
||||
{'key': 'Ctrl+Alt+4', 'style': 'h4'},
|
||||
{'key': 'Ctrl+Alt+5', 'style': 'h5'},
|
||||
{'key': 'Ctrl+Alt+6', 'style': 'h6'},
|
||||
{'key': 'Ctrl+Shift+L', 'style': 'link'},
|
||||
{'key': 'Ctrl+Shift+I', 'style': 'image'},
|
||||
{'key': 'Ctrl+Q', 'style': 'blockquote'},
|
||||
{'key': 'Ctrl+Shift+1', 'style': 'currentDate'},
|
||||
{'key': 'Ctrl+U', 'style': 'uppercase'},
|
||||
{'key': 'Ctrl+Shift+U', 'style': 'lowercase'},
|
||||
{'key': 'Ctrl+Alt+Shift+U', 'style': 'titlecase'},
|
||||
{'key': 'Ctrl+Alt+W', 'style': 'selectword'},
|
||||
{'key': 'Ctrl+L', 'style': 'list'},
|
||||
{'key': 'Ctrl+Alt+C', 'style': 'copyHTML'},
|
||||
{'key': 'Meta+Alt+C', 'style': 'copyHTML'},
|
||||
{'key': 'Meta+Enter', 'style': 'newLine'},
|
||||
{'key': 'Ctrl+Enter', 'style': 'newLine'}
|
||||
],
|
||||
imageMarkdownRegex = /^(?:\{<(.*?)>\})?!(?:\[([^\n\]]*)\])(?:\(([^\n\]]*)\))?$/gim,
|
||||
markerRegex = /\{<([\w\W]*?)>\}/;
|
||||
/*jslint regexp: false, bitwise: false */
|
||||
var PublishBar;
|
||||
|
||||
// The publish bar associated with a post, which has the TagWidget and
|
||||
// Save button and options and such.
|
||||
@ -47,364 +12,88 @@
|
||||
PublishBar = Ghost.View.extend({
|
||||
|
||||
initialize: function () {
|
||||
this.addSubview(new Ghost.View.EditorTagWidget({el: this.$('#entry-tags'), model: this.model})).render();
|
||||
this.addSubview(new ActionsWidget({el: this.$('#entry-actions'), model: this.model})).render();
|
||||
this.addSubview(new Ghost.View.PostSettings({el: $('#entry-controls'), model: this.model})).render();
|
||||
|
||||
this.addSubview(new Ghost.View.EditorTagWidget(
|
||||
{el: this.$('#entry-tags'), model: this.model}
|
||||
)).render();
|
||||
this.addSubview(new Ghost.View.PostSettings(
|
||||
{el: $('#entry-controls'), model: this.model}
|
||||
)).render();
|
||||
|
||||
// Pass the Actions widget references to the title and editor so that it can get
|
||||
// the values that need to be saved
|
||||
this.addSubview(new Ghost.View.EditorActionsWidget(
|
||||
{
|
||||
el: this.$('#entry-actions'),
|
||||
model: this.model,
|
||||
$title: this.options.$title,
|
||||
editor: this.options.editor
|
||||
}
|
||||
)).render();
|
||||
|
||||
},
|
||||
|
||||
render: function () { return this; }
|
||||
|
||||
});
|
||||
|
||||
// The Publish, Queue, Publish Now buttons
|
||||
// ----------------------------------------
|
||||
ActionsWidget = Ghost.View.extend({
|
||||
|
||||
events: {
|
||||
'click [data-set-status]': 'handleStatus',
|
||||
'click .js-publish-button': 'handlePostButton'
|
||||
},
|
||||
|
||||
statusMap: null,
|
||||
|
||||
createStatusMap: {
|
||||
'draft': 'Save Draft',
|
||||
'published': 'Publish Now'
|
||||
},
|
||||
|
||||
updateStatusMap: {
|
||||
'draft': 'Unpublish',
|
||||
'published': 'Update Post'
|
||||
},
|
||||
|
||||
//TODO: This has to be moved to the I18n localization file.
|
||||
//This structure is supposed to be close to the i18n-localization which will be used soon.
|
||||
messageMap: {
|
||||
errors: {
|
||||
post: {
|
||||
published: {
|
||||
'published': 'Your post could not be updated.',
|
||||
'draft': 'Your post could not be saved as a draft.'
|
||||
},
|
||||
draft: {
|
||||
'published': 'Your post could not be published.',
|
||||
'draft': 'Your post could not be saved as a draft.'
|
||||
}
|
||||
|
||||
}
|
||||
},
|
||||
|
||||
success: {
|
||||
post: {
|
||||
published: {
|
||||
'published': 'Your post has been updated.',
|
||||
'draft': 'Your post has been saved as a draft.'
|
||||
},
|
||||
draft: {
|
||||
'published': 'Your post has been published.',
|
||||
'draft': 'Your post has been saved as a draft.'
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
initialize: function () {
|
||||
var self = this;
|
||||
// Toggle publish
|
||||
shortcut.add("Ctrl+Alt+P", function () {
|
||||
self.toggleStatus();
|
||||
});
|
||||
shortcut.add("Ctrl+S", function () {
|
||||
self.updatePost();
|
||||
});
|
||||
shortcut.add("Meta+S", function () {
|
||||
self.updatePost();
|
||||
});
|
||||
this.listenTo(this.model, 'change:status', this.render);
|
||||
},
|
||||
|
||||
toggleStatus: function () {
|
||||
var self = this,
|
||||
keys = Object.keys(this.statusMap),
|
||||
model = self.model,
|
||||
prevStatus = model.get('status'),
|
||||
currentIndex = keys.indexOf(prevStatus),
|
||||
newIndex,
|
||||
status;
|
||||
|
||||
newIndex = currentIndex + 1 > keys.length - 1 ? 0 : currentIndex + 1;
|
||||
status = keys[newIndex];
|
||||
|
||||
this.setActiveStatus(keys[newIndex], this.statusMap[status], prevStatus);
|
||||
|
||||
this.savePost({
|
||||
status: keys[newIndex]
|
||||
}).then(function () {
|
||||
self.reportSaveSuccess(status, prevStatus);
|
||||
}, function (xhr) {
|
||||
// Show a notification about the error
|
||||
self.reportSaveError(xhr, model, status, prevStatus);
|
||||
});
|
||||
},
|
||||
|
||||
setActiveStatus: function (newStatus, displayText, currentStatus) {
|
||||
var isPublishing = (newStatus === 'published' && currentStatus !== 'published'),
|
||||
isUnpublishing = (newStatus === 'draft' && currentStatus === 'published'),
|
||||
// Controls when background of button has the splitbutton-delete/button-delete classes applied
|
||||
isImportantStatus = (isPublishing || isUnpublishing);
|
||||
|
||||
$('.js-publish-splitbutton')
|
||||
.removeClass(isImportantStatus ? 'splitbutton-save' : 'splitbutton-delete')
|
||||
.addClass(isImportantStatus ? 'splitbutton-delete' : 'splitbutton-save');
|
||||
|
||||
// Set the publish button's action and proper coloring
|
||||
$('.js-publish-button')
|
||||
.attr('data-status', newStatus)
|
||||
.text(displayText)
|
||||
.removeClass(isImportantStatus ? 'button-save' : 'button-delete')
|
||||
.addClass(isImportantStatus ? 'button-delete' : 'button-save');
|
||||
|
||||
// Remove the animated popup arrow
|
||||
$('.js-publish-splitbutton > a')
|
||||
.removeClass('active');
|
||||
|
||||
// Set the active action in the popup
|
||||
$('.js-publish-splitbutton .editor-options li')
|
||||
.removeClass('active')
|
||||
.filter(['li[data-set-status="', newStatus, '"]'].join(''))
|
||||
.addClass('active');
|
||||
},
|
||||
|
||||
handleStatus: function (e) {
|
||||
if (e) { e.preventDefault(); }
|
||||
var status = $(e.currentTarget).attr('data-set-status'),
|
||||
currentStatus = this.model.get('status');
|
||||
|
||||
this.setActiveStatus(status, this.statusMap[status], currentStatus);
|
||||
|
||||
// Dismiss the popup menu
|
||||
$('body').find('.overlay:visible').fadeOut();
|
||||
},
|
||||
|
||||
handlePostButton: function (e) {
|
||||
if (e) { e.preventDefault(); }
|
||||
var status = $(e.currentTarget).attr('data-status');
|
||||
|
||||
this.updatePost(status);
|
||||
},
|
||||
|
||||
updatePost: function (status) {
|
||||
var self = this,
|
||||
model = this.model,
|
||||
prevStatus = model.get('status');
|
||||
|
||||
// Default to same status if not passed in
|
||||
status = status || prevStatus;
|
||||
|
||||
model.trigger('willSave');
|
||||
|
||||
this.savePost({
|
||||
status: status
|
||||
}).then(function () {
|
||||
self.reportSaveSuccess(status, prevStatus);
|
||||
// Refresh publish button and all relevant controls with updated status.
|
||||
self.render();
|
||||
}, function (xhr) {
|
||||
// Set the model status back to previous
|
||||
model.set({ status: prevStatus });
|
||||
// Set appropriate button status
|
||||
self.setActiveStatus(status, self.statusMap[status], prevStatus);
|
||||
// Show a notification about the error
|
||||
self.reportSaveError(xhr, model, status, prevStatus);
|
||||
});
|
||||
},
|
||||
|
||||
savePost: function (data) {
|
||||
var publishButton = $('.js-publish-button'),
|
||||
saved,
|
||||
enablePublish = function (deferred) {
|
||||
deferred.always(function () {
|
||||
publishButton.prop('disabled', false);
|
||||
});
|
||||
return deferred;
|
||||
};
|
||||
|
||||
publishButton.prop('disabled', true);
|
||||
|
||||
_.each(this.model.blacklist, function (item) {
|
||||
this.model.unset(item);
|
||||
}, this);
|
||||
|
||||
saved = this.model.save(_.extend({
|
||||
title: $('#entry-title').val(),
|
||||
markdown: Ghost.currentView.getEditorValue()
|
||||
}, data));
|
||||
|
||||
// TODO: Take this out if #2489 gets merged in Backbone. Or patch Backbone
|
||||
// ourselves for more consistent promises.
|
||||
if (saved) {
|
||||
return enablePublish(saved);
|
||||
}
|
||||
|
||||
return enablePublish($.Deferred().reject());
|
||||
},
|
||||
|
||||
reportSaveSuccess: function (status, prevStatus) {
|
||||
Ghost.notifications.clearEverything();
|
||||
Ghost.notifications.addItem({
|
||||
type: 'success',
|
||||
message: this.messageMap.success.post[prevStatus][status],
|
||||
status: 'passive'
|
||||
});
|
||||
Ghost.currentView.setEditorDirty(false);
|
||||
},
|
||||
|
||||
reportSaveError: function (response, model, status, prevStatus) {
|
||||
var message = this.messageMap.errors.post[prevStatus][status];
|
||||
|
||||
if (response) {
|
||||
// Get message from response
|
||||
message += " " + Ghost.Views.Utils.getRequestErrorMessage(response);
|
||||
} else if (model.validationError) {
|
||||
// Grab a validation error
|
||||
message += " " + model.validationError;
|
||||
}
|
||||
|
||||
Ghost.notifications.clearEverything();
|
||||
Ghost.notifications.addItem({
|
||||
type: 'error',
|
||||
message: message,
|
||||
status: 'passive'
|
||||
});
|
||||
},
|
||||
|
||||
setStatusLabels: function (statusMap) {
|
||||
_.each(statusMap, function (label, status) {
|
||||
$('li[data-set-status="' + status + '"] > a').text(label);
|
||||
});
|
||||
},
|
||||
|
||||
render: function () {
|
||||
var status = this.model.get('status');
|
||||
|
||||
// Assume that we're creating a new post
|
||||
if (status !== 'published') {
|
||||
this.statusMap = this.createStatusMap;
|
||||
} else {
|
||||
this.statusMap = this.updateStatusMap;
|
||||
}
|
||||
|
||||
// Populate the publish menu with the appropriate verbiage
|
||||
this.setStatusLabels(this.statusMap);
|
||||
|
||||
// Default the selected publish option to the current status of the post.
|
||||
this.setActiveStatus(status, this.statusMap[status], status);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
// The entire /editor page's route
|
||||
// ----------------------------------------
|
||||
Ghost.Views.Editor = Ghost.View.extend({
|
||||
|
||||
initialize: function () {
|
||||
var self = this;
|
||||
|
||||
// Add the container view for the Publish Bar
|
||||
this.addSubview(new PublishBar({el: "#publish-bar", model: this.model})).render();
|
||||
|
||||
this.$('#entry-title').val(this.model.get('title')).focus();
|
||||
this.$('#entry-markdown').text(this.model.get('markdown'));
|
||||
|
||||
this.listenTo(this.model, 'change:title', this.renderTitle);
|
||||
this.listenTo(this.model, 'change:id', function (m) {
|
||||
// This is a special case for browsers which fire an unload event when using navigate. The id change
|
||||
// happens before the save success and can cause the unload alert to appear incorrectly on first save
|
||||
// The id only changes in the event that the save has been successful, so this workaround is safes
|
||||
self.setEditorDirty(false);
|
||||
Backbone.history.navigate('/editor/' + m.id + '/');
|
||||
});
|
||||
|
||||
this.initMarkdown();
|
||||
this.renderPreview();
|
||||
|
||||
$('.entry-content header, .entry-preview header').on('click', function () {
|
||||
$('.entry-content, .entry-preview').removeClass('active');
|
||||
$(this).closest('section').addClass('active');
|
||||
});
|
||||
|
||||
$('.entry-title .icon-fullscreen').on('click', function (e) {
|
||||
e.preventDefault();
|
||||
$('body').toggleClass('fullscreen');
|
||||
});
|
||||
|
||||
this.$('.CodeMirror-scroll').on('scroll', this.syncScroll);
|
||||
|
||||
this.$('.CodeMirror-scroll').scrollClass({target: '.entry-markdown', offset: 10});
|
||||
this.$('.entry-preview-content').scrollClass({target: '.entry-preview', offset: 10});
|
||||
|
||||
|
||||
// Zen writing mode shortcut
|
||||
shortcut.add("Alt+Shift+Z", function () {
|
||||
$('body').toggleClass('zen');
|
||||
});
|
||||
|
||||
$('.entry-markdown header, .entry-preview header').click(function (e) {
|
||||
$('.entry-markdown, .entry-preview').removeClass('active');
|
||||
$(e.target).closest('section').addClass('active');
|
||||
});
|
||||
|
||||
// Deactivate default drag/drop action
|
||||
$(document).bind('drop dragover', function (e) {
|
||||
e.preventDefault();
|
||||
});
|
||||
},
|
||||
|
||||
events: {
|
||||
'click .markdown-help': 'showHelp',
|
||||
'blur #entry-title': 'trimTitle',
|
||||
'orientationchange': 'orientationChange'
|
||||
},
|
||||
|
||||
syncScroll: _.throttle(function (e) {
|
||||
var $codeViewport = $(e.target),
|
||||
$previewViewport = $('.entry-preview-content'),
|
||||
$codeContent = $('.CodeMirror-sizer'),
|
||||
$previewContent = $('.rendered-markdown'),
|
||||
initialize: function () {
|
||||
this.$title = this.$('#entry-title');
|
||||
this.$editor = this.$('#entry-markdown');
|
||||
|
||||
// calc position
|
||||
codeHeight = $codeContent.height() - $codeViewport.height(),
|
||||
previewHeight = $previewContent.height() - $previewViewport.height(),
|
||||
ratio = previewHeight / codeHeight,
|
||||
previewPostition = $codeViewport.scrollTop() * ratio;
|
||||
this.$title.val(this.model.get('title')).focus();
|
||||
this.$editor.text(this.model.get('markdown'));
|
||||
|
||||
// apply new scroll
|
||||
$previewViewport.scrollTop(previewPostition);
|
||||
}, 10),
|
||||
// Create a new editor
|
||||
this.editor = new Ghost.Editor.Main();
|
||||
|
||||
showHelp: function () {
|
||||
this.addSubview(new Ghost.Views.Modal({
|
||||
model: {
|
||||
options: {
|
||||
close: true,
|
||||
style: ["wide"],
|
||||
animation: 'fade'
|
||||
},
|
||||
content: {
|
||||
template: 'markdown',
|
||||
title: 'Markdown Help'
|
||||
}
|
||||
}
|
||||
}));
|
||||
// Add the container view for the Publish Bar
|
||||
// Passing reference to the title and editor
|
||||
this.addSubview(new PublishBar(
|
||||
{el: '#publish-bar', model: this.model, $title: this.$title, editor: this.editor}
|
||||
)).render();
|
||||
|
||||
this.listenTo(this.model, 'change:title', this.renderTitle);
|
||||
this.listenTo(this.model, 'change:id', this.handleIdChange);
|
||||
|
||||
this.bindShortcuts();
|
||||
|
||||
$('.entry-markdown header, .entry-preview header').on('click', function (e) {
|
||||
$('.entry-markdown, .entry-preview').removeClass('active');
|
||||
$(e.currentTarget).closest('section').addClass('active');
|
||||
});
|
||||
},
|
||||
|
||||
bindShortcuts: function () {
|
||||
var self = this;
|
||||
|
||||
// Zen writing mode shortcut - full editor view
|
||||
shortcut.add('Alt+Shift+Z', function () {
|
||||
$('body').toggleClass('zen');
|
||||
});
|
||||
|
||||
// HTML copy & paste
|
||||
shortcut.add('Ctrl+Alt+C', function () {
|
||||
self.showHTML();
|
||||
});
|
||||
},
|
||||
|
||||
trimTitle: function () {
|
||||
var $title = $('#entry-title'),
|
||||
rawTitle = $title.val(),
|
||||
var rawTitle = this.$title.val(),
|
||||
trimmedTitle = $.trim(rawTitle);
|
||||
|
||||
if (rawTitle !== trimmedTitle) {
|
||||
$title.val(trimmedTitle);
|
||||
this.$title.val(trimmedTitle);
|
||||
}
|
||||
|
||||
// Trigger title change for post-settings.js
|
||||
@ -412,7 +101,15 @@
|
||||
},
|
||||
|
||||
renderTitle: function () {
|
||||
this.$('#entry-title').val(this.model.get('title'));
|
||||
this.$title.val(this.model.get('title'));
|
||||
},
|
||||
|
||||
handleIdChange: function (m) {
|
||||
// This is a special case for browsers which fire an unload event when using navigate. The id change
|
||||
// happens before the save success and can cause the unload alert to appear incorrectly on first save
|
||||
// The id only changes in the event that the save has been successful, so this workaround is safes
|
||||
this.editor.setDirty(false);
|
||||
Backbone.history.navigate('/editor/' + m.id + '/');
|
||||
},
|
||||
|
||||
// This is a hack to remove iOS6 white space on orientation change bug
|
||||
@ -427,307 +124,35 @@
|
||||
}
|
||||
},
|
||||
|
||||
// This updates the editor preview panel.
|
||||
// Currently gets called on every key press.
|
||||
// Also trigger word count update
|
||||
renderPreview: function () {
|
||||
var self = this,
|
||||
preview = document.getElementsByClassName('rendered-markdown')[0];
|
||||
preview.innerHTML = this.converter.makeHtml(this.editor.getValue());
|
||||
|
||||
this.initUploads();
|
||||
|
||||
Countable.once(preview, function (counter) {
|
||||
self.$('.entry-word-count').text($.pluralize(counter.words, 'word'));
|
||||
self.$('.entry-character-count').text($.pluralize(counter.characters, 'character'));
|
||||
self.$('.entry-paragraph-count').text($.pluralize(counter.paragraphs, 'paragraph'));
|
||||
});
|
||||
},
|
||||
|
||||
// Markdown converter & markdown shortcut initialization.
|
||||
initMarkdown: function () {
|
||||
var self = this;
|
||||
|
||||
this.converter = new Showdown.converter({extensions: ['typography', 'ghostdown', 'github']});
|
||||
this.editor = CodeMirror.fromTextArea(document.getElementById('entry-markdown'), {
|
||||
mode: 'gfm',
|
||||
tabMode: 'indent',
|
||||
tabindex: "2",
|
||||
lineWrapping: true,
|
||||
dragDrop: false,
|
||||
extraKeys: {
|
||||
Home: "goLineLeft",
|
||||
End: "goLineRight"
|
||||
}
|
||||
});
|
||||
this.uploadMgr = new UploadManager(this.editor);
|
||||
|
||||
// Inject modal for HTML to be viewed in
|
||||
shortcut.add("Ctrl+Alt+C", function () {
|
||||
self.showHTML();
|
||||
});
|
||||
shortcut.add("Ctrl+Alt+C", function () {
|
||||
self.showHTML();
|
||||
});
|
||||
|
||||
_.each(MarkdownShortcuts, function (combo) {
|
||||
shortcut.add(combo.key, function () {
|
||||
return self.editor.addMarkdown({style: combo.style});
|
||||
});
|
||||
});
|
||||
|
||||
this.enableEditor();
|
||||
},
|
||||
|
||||
options: {
|
||||
markers: {}
|
||||
},
|
||||
|
||||
getEditorValue: function () {
|
||||
return this.uploadMgr.getEditorValue();
|
||||
},
|
||||
|
||||
unloadDirtyMessage: function () {
|
||||
return "==============================\n\n" +
|
||||
"Hey there! It looks like you're in the middle of writing" +
|
||||
" something and you haven't saved all of your content." +
|
||||
"\n\nSave before you go!\n\n" +
|
||||
"==============================";
|
||||
},
|
||||
|
||||
setEditorDirty: function (dirty) {
|
||||
window.onbeforeunload = dirty ? this.unloadDirtyMessage : null;
|
||||
},
|
||||
|
||||
initUploads: function () {
|
||||
var filestorage = $('#entry-markdown-content').data('filestorage');
|
||||
this.$('.js-drop-zone').upload({editor: true, fileStorage: filestorage});
|
||||
this.$('.js-drop-zone').on('uploadstart', $.proxy(this.disableEditor, this));
|
||||
this.$('.js-drop-zone').on('uploadfailure', $.proxy(this.enableEditor, this));
|
||||
this.$('.js-drop-zone').on('uploadsuccess', $.proxy(this.enableEditor, this));
|
||||
this.$('.js-drop-zone').on('uploadsuccess', this.uploadMgr.handleUpload);
|
||||
},
|
||||
|
||||
enableEditor: function () {
|
||||
var self = this;
|
||||
this.editor.setOption("readOnly", false);
|
||||
this.editor.on('change', function () {
|
||||
self.setEditorDirty(true);
|
||||
self.renderPreview();
|
||||
});
|
||||
},
|
||||
|
||||
disableEditor: function () {
|
||||
var self = this;
|
||||
this.editor.setOption("readOnly", "nocursor");
|
||||
this.editor.off('change', function () {
|
||||
self.renderPreview();
|
||||
});
|
||||
},
|
||||
|
||||
showHTML: function () {
|
||||
showEditorModal: function (content) {
|
||||
this.addSubview(new Ghost.Views.Modal({
|
||||
model: {
|
||||
options: {
|
||||
close: true,
|
||||
style: ["wide"],
|
||||
style: ['wide'],
|
||||
animation: 'fade'
|
||||
},
|
||||
content: {
|
||||
template: 'copyToHTML',
|
||||
title: 'Copied HTML'
|
||||
}
|
||||
content: content
|
||||
}
|
||||
}));
|
||||
},
|
||||
|
||||
showHelp: function () {
|
||||
var content = {
|
||||
template: 'markdown',
|
||||
title: 'Markdown Help'
|
||||
};
|
||||
this.showEditorModal(content);
|
||||
},
|
||||
|
||||
showHTML: function () {
|
||||
var content = {
|
||||
template: 'copyToHTML',
|
||||
title: 'Copied HTML'
|
||||
};
|
||||
this.showEditorModal(content);
|
||||
},
|
||||
|
||||
render: function () { return this; }
|
||||
});
|
||||
|
||||
MarkerManager = function (editor) {
|
||||
var markers = {},
|
||||
uploadPrefix = 'image_upload',
|
||||
uploadId = 1;
|
||||
|
||||
function addMarker(line, ln) {
|
||||
var marker,
|
||||
magicId = '{<' + uploadId + '>}';
|
||||
editor.setLine(ln, magicId + line.text);
|
||||
marker = editor.markText(
|
||||
{line: ln, ch: 0},
|
||||
{line: ln, ch: (magicId.length)},
|
||||
{collapsed: true}
|
||||
);
|
||||
|
||||
markers[uploadPrefix + '_' + uploadId] = marker;
|
||||
uploadId += 1;
|
||||
}
|
||||
|
||||
function getMarkerRegexForId(id) {
|
||||
id = id.replace('image_upload_', '');
|
||||
return new RegExp('\\{<' + id + '>\\}', 'gmi');
|
||||
}
|
||||
|
||||
function stripMarkerFromLine(line) {
|
||||
var markerText = line.text.match(markerRegex),
|
||||
ln = editor.getLineNumber(line);
|
||||
|
||||
if (markerText) {
|
||||
editor.replaceRange('', {line: ln, ch: markerText.index}, {line: ln, ch: markerText.index + markerText[0].length});
|
||||
}
|
||||
}
|
||||
|
||||
function findAndStripMarker(id) {
|
||||
editor.eachLine(function (line) {
|
||||
var markerText = getMarkerRegexForId(id).exec(line.text),
|
||||
ln;
|
||||
|
||||
if (markerText) {
|
||||
ln = editor.getLineNumber(line);
|
||||
editor.replaceRange('', {line: ln, ch: markerText.index}, {line: ln, ch: markerText.index + markerText[0].length});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function removeMarker(id, marker, line) {
|
||||
delete markers[id];
|
||||
marker.clear();
|
||||
|
||||
if (line) {
|
||||
stripMarkerFromLine(line);
|
||||
} else {
|
||||
findAndStripMarker(id);
|
||||
}
|
||||
}
|
||||
|
||||
function checkMarkers() {
|
||||
_.each(markers, function (marker, id) {
|
||||
var line;
|
||||
marker = markers[id];
|
||||
if (marker.find()) {
|
||||
line = editor.getLineHandle(marker.find().from.line);
|
||||
if (!line.text.match(imageMarkdownRegex)) {
|
||||
removeMarker(id, marker, line);
|
||||
}
|
||||
} else {
|
||||
removeMarker(id, marker);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function initMarkers(line) {
|
||||
var isImage = line.text.match(imageMarkdownRegex),
|
||||
hasMarker = line.text.match(markerRegex);
|
||||
|
||||
if (isImage && !hasMarker) {
|
||||
addMarker(line, editor.getLineNumber(line));
|
||||
}
|
||||
}
|
||||
|
||||
// public api
|
||||
_.extend(this, {
|
||||
markers: markers,
|
||||
checkMarkers: checkMarkers,
|
||||
addMarker: addMarker,
|
||||
stripMarkerFromLine: stripMarkerFromLine,
|
||||
getMarkerRegexForId: getMarkerRegexForId
|
||||
});
|
||||
|
||||
// Initialise
|
||||
editor.eachLine(initMarkers);
|
||||
};
|
||||
|
||||
UploadManager = function (editor) {
|
||||
var markerMgr = new MarkerManager(editor);
|
||||
|
||||
function findLine(result_id) {
|
||||
// try to find the right line to replace
|
||||
if (markerMgr.markers.hasOwnProperty(result_id) && markerMgr.markers[result_id].find()) {
|
||||
return editor.getLineHandle(markerMgr.markers[result_id].find().from.line);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function checkLine(ln, mode) {
|
||||
var line = editor.getLineHandle(ln),
|
||||
isImage = line.text.match(imageMarkdownRegex),
|
||||
hasMarker;
|
||||
|
||||
// We care if it is an image
|
||||
if (isImage) {
|
||||
hasMarker = line.text.match(markerRegex);
|
||||
|
||||
if (hasMarker && mode === 'paste') {
|
||||
// this could be a duplicate, and won't be a real marker
|
||||
markerMgr.stripMarkerFromLine(line);
|
||||
}
|
||||
|
||||
if (!hasMarker) {
|
||||
markerMgr.addMarker(line, ln);
|
||||
}
|
||||
}
|
||||
// TODO: hasMarker but no image?
|
||||
}
|
||||
|
||||
function handleUpload(e, result_src) {
|
||||
/*jslint regexp: true, bitwise: true */
|
||||
var line = findLine($(e.currentTarget).attr('id')),
|
||||
lineNumber = editor.getLineNumber(line),
|
||||
match = line.text.match(/\([^\n]*\)?/),
|
||||
replacement = '(http://)';
|
||||
/*jslint regexp: false, bitwise: false */
|
||||
|
||||
if (match) {
|
||||
// simple case, we have the parenthesis
|
||||
editor.setSelection({line: lineNumber, ch: match.index + 1}, {line: lineNumber, ch: match.index + match[0].length - 1});
|
||||
} else {
|
||||
match = line.text.match(/\]/);
|
||||
if (match) {
|
||||
editor.replaceRange(
|
||||
replacement,
|
||||
{line: lineNumber, ch: match.index + 1},
|
||||
{line: lineNumber, ch: match.index + 1}
|
||||
);
|
||||
editor.setSelection(
|
||||
{line: lineNumber, ch: match.index + 2},
|
||||
{line: lineNumber, ch: match.index + replacement.length }
|
||||
);
|
||||
}
|
||||
}
|
||||
editor.replaceSelection(result_src);
|
||||
}
|
||||
|
||||
function getEditorValue() {
|
||||
var value = editor.getValue();
|
||||
|
||||
_.each(markerMgr.markers, function (marker, id) {
|
||||
/*jshint unused:false*/
|
||||
value = value.replace(markerMgr.getMarkerRegexForId(id), '');
|
||||
});
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
|
||||
// Public API
|
||||
_.extend(this, {
|
||||
getEditorValue: getEditorValue,
|
||||
handleUpload: handleUpload
|
||||
});
|
||||
|
||||
// initialise
|
||||
editor.on('change', function (cm, changeObj) {
|
||||
/*jshint unused:false*/
|
||||
var linesChanged = _.range(changeObj.from.line, changeObj.from.line + changeObj.text.length);
|
||||
|
||||
_.each(linesChanged, function (ln) {
|
||||
checkLine(ln, changeObj.origin);
|
||||
});
|
||||
|
||||
// Is this a line which may have had a marker on it?
|
||||
markerMgr.checkMarkers();
|
||||
});
|
||||
};
|
||||
|
||||
}());
|
||||
}());
|
@ -94,7 +94,8 @@
|
||||
var name = this.$('.name').val(),
|
||||
email = this.$('.email').val(),
|
||||
password = this.$('.password').val(),
|
||||
validationErrors = [];
|
||||
validationErrors = [],
|
||||
self = this;
|
||||
|
||||
if (!validator.isLength(name, 1)) {
|
||||
validationErrors.push("Please enter a name.");
|
||||
@ -131,7 +132,7 @@
|
||||
window.location.href = msg.redirect;
|
||||
},
|
||||
error: function (xhr) {
|
||||
this.submitted = "no";
|
||||
self.submitted = "no";
|
||||
Ghost.notifications.clearEverything();
|
||||
Ghost.notifications.addItem({
|
||||
type: 'error',
|
||||
|
@ -76,7 +76,7 @@
|
||||
// and then update the placeholder value.
|
||||
if (title) {
|
||||
$.ajax({
|
||||
url: Ghost.paths.apiRoot + '/posts/getSlug/' + encodeURIComponent(title) + '/',
|
||||
url: Ghost.paths.apiRoot + '/posts/slug/' + encodeURIComponent(title) + '/',
|
||||
success: function (result) {
|
||||
$postSettingSlugEl.attr('placeholder', result);
|
||||
}
|
||||
|
@ -220,14 +220,8 @@
|
||||
} else {
|
||||
data[key] = this.$('.js-upload-target').attr('src');
|
||||
}
|
||||
|
||||
self.model.save(data, {
|
||||
success: self.saveSuccess,
|
||||
error: self.saveError
|
||||
}).then(function () {
|
||||
self.saveSettings();
|
||||
});
|
||||
|
||||
self.model.set(data);
|
||||
self.saveSettings();
|
||||
return true;
|
||||
},
|
||||
buttonClass: "button-save right",
|
||||
@ -298,12 +292,8 @@
|
||||
} else {
|
||||
data[key] = this.$('.js-upload-target').attr('src');
|
||||
}
|
||||
self.model.save(data, {
|
||||
success: self.saveSuccess,
|
||||
error: self.saveError
|
||||
}).then(function () {
|
||||
self.saveUser();
|
||||
});
|
||||
self.model.set(data);
|
||||
self.saveUser(data);
|
||||
return true;
|
||||
},
|
||||
buttonClass: "button-save right",
|
||||
|
@ -38,7 +38,7 @@ db = {
|
||||
return api.settings.read({ key: 'databaseVersion' }).then(function (setting) {
|
||||
return when(setting.value);
|
||||
}, function () {
|
||||
return when('001');
|
||||
return when('002');
|
||||
}).then(function (version) {
|
||||
databaseVersion = version;
|
||||
// Read the file contents
|
||||
@ -82,6 +82,9 @@ db = {
|
||||
return when.resolve({message: 'Posts, tags and other data successfully imported'});
|
||||
}).otherwise(function importFailure(error) {
|
||||
return when.reject({code: 500, message: error.message || error});
|
||||
}).finally(function () {
|
||||
// Unlink the file after import
|
||||
return nodefn.call(fs.unlink, options.importfile.path);
|
||||
});
|
||||
},
|
||||
'deleteAllContent': function () {
|
||||
|
@ -26,9 +26,9 @@ function cacheInvalidationHeader(req, result) {
|
||||
|
||||
if (method === 'POST' || method === 'PUT' || method === 'DELETE') {
|
||||
if (endpoint === 'settings' || endpoint === 'users' || endpoint === 'db') {
|
||||
cacheInvalidate = "/*";
|
||||
cacheInvalidate = '/*';
|
||||
} else if (endpoint === 'posts') {
|
||||
cacheInvalidate = "/, /page/*, /rss/, /rss/*";
|
||||
cacheInvalidate = '/, /page/*, /rss/, /rss/*, /tag/*';
|
||||
if (id && jsonResult.slug) {
|
||||
return config.urlForPost(settings, jsonResult).then(function (postUrl) {
|
||||
return cacheInvalidate + ', ' + postUrl;
|
||||
|
@ -47,7 +47,7 @@ posts = {
|
||||
});
|
||||
},
|
||||
|
||||
getSlug: function getSlug(args) {
|
||||
generateSlug: function getSlug(args) {
|
||||
return dataProvider.Base.Model.generateSlug(dataProvider.Post, args.title, {status: 'all'}).then(function (slug) {
|
||||
if (slug) {
|
||||
return slug;
|
||||
|
@ -9,7 +9,6 @@ var _ = require('lodash'),
|
||||
|
||||
// Paths for views
|
||||
defaultErrorTemplatePath = path.resolve(config().paths.adminViews, 'user-error.hbs'),
|
||||
userErrorTemplatePath = path.resolve(config().paths.themePath, 'error.hbs'),
|
||||
userErrorTemplateExists = false;
|
||||
|
||||
// This is not useful but required for jshint
|
||||
@ -19,9 +18,8 @@ colors.setTheme({silly: 'rainbow'});
|
||||
* Basic error handling helpers
|
||||
*/
|
||||
errors = {
|
||||
updateActiveTheme: function (activeTheme, hasErrorTemplate) {
|
||||
userErrorTemplatePath = path.resolve(config().paths.themePath, activeTheme, 'error.hbs');
|
||||
userErrorTemplateExists = hasErrorTemplate;
|
||||
updateActiveTheme: function (activeTheme) {
|
||||
userErrorTemplateExists = config().paths.availableThemes[activeTheme].hasOwnProperty('error.hbs');
|
||||
},
|
||||
|
||||
throwError: function (err) {
|
||||
@ -47,23 +45,27 @@ errors = {
|
||||
process.env.NODE_ENV === 'staging' ||
|
||||
process.env.NODE_ENV === 'production')) {
|
||||
|
||||
console.log('\nWarning:'.yellow, warn.yellow);
|
||||
var msgs = ['\nWarning:'.yellow, warn.yellow, '\n'];
|
||||
|
||||
if (context) {
|
||||
console.log(context.white);
|
||||
msgs.push(context.white, '\n');
|
||||
}
|
||||
|
||||
if (help) {
|
||||
console.log(help.green);
|
||||
msgs.push(help.green);
|
||||
}
|
||||
|
||||
// add a new line
|
||||
console.log('');
|
||||
msgs.push('\n');
|
||||
|
||||
console.log.apply(console, msgs);
|
||||
}
|
||||
},
|
||||
|
||||
logError: function (err, context, help) {
|
||||
var stack = err ? err.stack : null;
|
||||
var stack = err ? err.stack : null,
|
||||
msgs;
|
||||
|
||||
if (err) {
|
||||
err = err.message || err || 'An unknown error occurred.';
|
||||
} else {
|
||||
@ -75,22 +77,24 @@ errors = {
|
||||
process.env.NODE_ENV === 'staging' ||
|
||||
process.env.NODE_ENV === 'production')) {
|
||||
|
||||
console.error('\nERROR:'.red, err.red);
|
||||
msgs = ['\nERROR:'.red, err.red, '\n'];
|
||||
|
||||
if (context) {
|
||||
console.error(context.white);
|
||||
msgs.push(context.white, '\n');
|
||||
}
|
||||
|
||||
if (help) {
|
||||
console.error(help.green);
|
||||
msgs.push(help.green);
|
||||
}
|
||||
|
||||
// add a new line
|
||||
console.error('');
|
||||
msgs.push('\n');
|
||||
|
||||
if (stack) {
|
||||
console.error(stack, '\n');
|
||||
msgs.push(stack, '\n');
|
||||
}
|
||||
|
||||
console.error.apply(console, msgs);
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -121,7 +121,7 @@ coreHelpers.page_url = function (context, block) {
|
||||
//
|
||||
coreHelpers.pageUrl = function (context, block) {
|
||||
errors.logWarn('Warning: pageUrl is deprecated, please use page_url instead\n' +
|
||||
'The helper pageUrl has been replaced with page_url in Ghost 0.5, and will be removed entirely in Ghost 0.6\n' +
|
||||
'The helper pageUrl has been replaced with page_url in Ghost 0.4.2, and will be removed entirely in Ghost 0.6\n' +
|
||||
'In your theme\'s pagination.hbs file, pageUrl should be renamed to page_url');
|
||||
|
||||
/*jshint unused:false*/
|
||||
@ -368,14 +368,19 @@ coreHelpers.body_class = function (options) {
|
||||
tags = this.post && this.post.tags ? this.post.tags : this.tags || [],
|
||||
page = this.post && this.post.page ? this.post.page : this.page || false;
|
||||
|
||||
if (_.isString(this.relativeUrl) && this.relativeUrl.match(/\/(page|tag)/)) {
|
||||
if (_.isString(this.relativeUrl) && this.relativeUrl.match(/\/(page\/\d)/)) {
|
||||
classes.push('archive-template');
|
||||
} else if (!this.relativeUrl || this.relativeUrl === '/' || this.relativeUrl === '') {
|
||||
classes.push('home-template');
|
||||
} else {
|
||||
} else if (post) {
|
||||
classes.push('post-template');
|
||||
}
|
||||
|
||||
if (this.tag !== undefined) {
|
||||
classes.push('tag-template');
|
||||
classes.push('tag-' + this.tag.slug);
|
||||
}
|
||||
|
||||
if (tags) {
|
||||
classes = classes.concat(tags.map(function (tag) { return 'tag-' + tag.slug; }));
|
||||
}
|
||||
@ -459,10 +464,11 @@ coreHelpers.ghost_head = function (options) {
|
||||
|
||||
coreHelpers.ghost_foot = function (options) {
|
||||
/*jshint unused:false*/
|
||||
var foot = [];
|
||||
var jquery = isProduction ? 'jquery.min.js' : 'jquery.js',
|
||||
foot = [];
|
||||
|
||||
foot.push(scriptTemplate({
|
||||
source: config().paths.subdir + '/public/jquery.js',
|
||||
source: config().paths.subdir + '/public/' + jquery,
|
||||
version: coreHelpers.assetHash
|
||||
}));
|
||||
|
||||
|
@ -22,6 +22,7 @@ var crypto = require('crypto'),
|
||||
packageInfo = require('../../package.json'),
|
||||
|
||||
// Variables
|
||||
httpServer,
|
||||
dbHash;
|
||||
|
||||
// If we're in development mode, require "when/console/monitor"
|
||||
@ -105,63 +106,58 @@ function builtFilesExist() {
|
||||
return when.all(deferreds);
|
||||
}
|
||||
|
||||
function startGhost(deferred) {
|
||||
function ghostStartMessages() {
|
||||
// Tell users if their node version is not supported, and exit
|
||||
if (!semver.satisfies(process.versions.node, packageInfo.engines.node)) {
|
||||
console.log(
|
||||
"\nERROR: Unsupported version of Node".red,
|
||||
"\nGhost needs Node version".red,
|
||||
packageInfo.engines.node.yellow,
|
||||
"you are using version".red,
|
||||
process.versions.node.yellow,
|
||||
"\nPlease go to http://nodejs.org to get a supported version".green
|
||||
);
|
||||
|
||||
return function () {
|
||||
// Tell users if their node version is not supported, and exit
|
||||
if (!semver.satisfies(process.versions.node, packageInfo.engines.node)) {
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// Startup & Shutdown messages
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
console.log(
|
||||
"Ghost is running...".green,
|
||||
"\nYour blog is now available on",
|
||||
config().url,
|
||||
"\nCtrl+C to shut down".grey
|
||||
);
|
||||
|
||||
// ensure that Ghost exits correctly on Ctrl+C
|
||||
process.on('SIGINT', function () {
|
||||
console.log(
|
||||
"\nERROR: Unsupported version of Node".red,
|
||||
"\nGhost needs Node version".red,
|
||||
packageInfo.engines.node.yellow,
|
||||
"you are using version".red,
|
||||
process.versions.node.yellow,
|
||||
"\nPlease go to http://nodejs.org to get a supported version".green
|
||||
"\nGhost has shut down".red,
|
||||
"\nYour blog is now offline"
|
||||
);
|
||||
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// Startup & Shutdown messages
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
console.log(
|
||||
"Ghost is running...".green,
|
||||
"\nYour blog is now available on",
|
||||
config().url,
|
||||
"\nCtrl+C to shut down".grey
|
||||
);
|
||||
|
||||
// ensure that Ghost exits correctly on Ctrl+C
|
||||
process.on('SIGINT', function () {
|
||||
console.log(
|
||||
"\nGhost has shut down".red,
|
||||
"\nYour blog is now offline"
|
||||
);
|
||||
process.exit(0);
|
||||
});
|
||||
} else {
|
||||
console.log(
|
||||
("Ghost is running in " + process.env.NODE_ENV + "...").green,
|
||||
"\nListening on",
|
||||
});
|
||||
} else {
|
||||
console.log(
|
||||
("Ghost is running in " + process.env.NODE_ENV + "...").green,
|
||||
"\nListening on",
|
||||
config.getSocket() || config().server.host + ':' + config().server.port,
|
||||
"\nUrl configured as:",
|
||||
config().url,
|
||||
"\nCtrl+C to shut down".grey
|
||||
"\nUrl configured as:",
|
||||
config().url,
|
||||
"\nCtrl+C to shut down".grey
|
||||
);
|
||||
// ensure that Ghost exits correctly on Ctrl+C
|
||||
process.on('SIGINT', function () {
|
||||
console.log(
|
||||
"\nGhost has shutdown".red,
|
||||
"\nGhost was running for",
|
||||
Math.round(process.uptime()),
|
||||
"seconds"
|
||||
);
|
||||
// ensure that Ghost exits correctly on Ctrl+C
|
||||
process.on('SIGINT', function () {
|
||||
console.log(
|
||||
"\nGhost has shutdown".red,
|
||||
"\nGhost was running for",
|
||||
Math.round(process.uptime()),
|
||||
"seconds"
|
||||
);
|
||||
process.exit(0);
|
||||
});
|
||||
}
|
||||
|
||||
deferred.resolve();
|
||||
};
|
||||
process.exit(0);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ## Initializes the ghost application.
|
||||
@ -246,11 +242,11 @@ function init(server) {
|
||||
|
||||
// Log all theme errors and warnings
|
||||
_.each(config().paths.availableThemes._messages.errors, function (error) {
|
||||
errors.logError(error.message, error.context);
|
||||
errors.logError(error.message, error.context, error.help);
|
||||
});
|
||||
|
||||
_.each(config().paths.availableThemes._messages.warns, function (warn) {
|
||||
errors.logWarn(warn.message, warn.context);
|
||||
errors.logWarn(warn.message, warn.context, warn.help);
|
||||
});
|
||||
|
||||
// ## Start Ghost App
|
||||
@ -258,21 +254,25 @@ function init(server) {
|
||||
// Make sure the socket is gone before trying to create another
|
||||
fs.unlink(config.getSocket(), function (err) {
|
||||
/*jshint unused:false*/
|
||||
server.listen(
|
||||
config.getSocket(),
|
||||
startGhost(deferred)
|
||||
httpServer = server.listen(
|
||||
config.getSocket()
|
||||
);
|
||||
fs.chmod(config.getSocket(), '0660');
|
||||
});
|
||||
|
||||
} else {
|
||||
server.listen(
|
||||
httpServer = server.listen(
|
||||
config().server.port,
|
||||
config().server.host,
|
||||
startGhost(deferred)
|
||||
config().server.host
|
||||
);
|
||||
}
|
||||
|
||||
httpServer.on('listening', function () {
|
||||
ghostStartMessages();
|
||||
deferred.resolve(httpServer);
|
||||
});
|
||||
|
||||
|
||||
return deferred.promise;
|
||||
});
|
||||
}
|
||||
|
@ -118,7 +118,7 @@ function activateTheme(activeTheme) {
|
||||
expressServer.set('theme view engine', hbs.express3(hbsOptions));
|
||||
|
||||
// Update user error template
|
||||
errors.updateActiveTheme(activeTheme, config().paths.availableThemes[activeTheme].hasOwnProperty('error'));
|
||||
errors.updateActiveTheme(activeTheme);
|
||||
}
|
||||
|
||||
// ### ManageAdminAndTheme Middleware
|
||||
|
@ -199,36 +199,39 @@ ghostBookshelf.Model = ghostBookshelf.Model.extend({
|
||||
generateSlug: function (Model, base, readOptions) {
|
||||
var slug,
|
||||
slugTryCount = 1,
|
||||
baseName = Model.prototype.tableName.replace(/s$/, ''),
|
||||
// Look for a post with a matching slug, append an incrementing number if so
|
||||
checkIfSlugExists = function (slugToFind) {
|
||||
var args = {slug: slugToFind};
|
||||
//status is needed for posts
|
||||
if (readOptions && readOptions.status) {
|
||||
args.status = readOptions.status;
|
||||
checkIfSlugExists;
|
||||
|
||||
checkIfSlugExists = function (slugToFind) {
|
||||
var args = {slug: slugToFind};
|
||||
//status is needed for posts
|
||||
if (readOptions && readOptions.status) {
|
||||
args.status = readOptions.status;
|
||||
}
|
||||
return Model.findOne(args, readOptions).then(function (found) {
|
||||
var trimSpace;
|
||||
|
||||
if (!found) {
|
||||
return when.resolve(slugToFind);
|
||||
}
|
||||
return Model.findOne(args, readOptions).then(function (found) {
|
||||
var trimSpace;
|
||||
|
||||
if (!found) {
|
||||
return when.resolve(slugToFind);
|
||||
}
|
||||
slugTryCount += 1;
|
||||
|
||||
slugTryCount += 1;
|
||||
// If this is the first time through, add the hyphen
|
||||
if (slugTryCount === 2) {
|
||||
slugToFind += '-';
|
||||
} else {
|
||||
// Otherwise, trim the number off the end
|
||||
trimSpace = -(String(slugTryCount - 1).length);
|
||||
slugToFind = slugToFind.slice(0, trimSpace);
|
||||
}
|
||||
|
||||
// If this is the first time through, add the hyphen
|
||||
if (slugTryCount === 2) {
|
||||
slugToFind += '-';
|
||||
} else {
|
||||
// Otherwise, trim the number off the end
|
||||
trimSpace = -(String(slugTryCount - 1).length);
|
||||
slugToFind = slugToFind.slice(0, trimSpace);
|
||||
}
|
||||
slugToFind += slugTryCount;
|
||||
|
||||
slugToFind += slugTryCount;
|
||||
|
||||
return checkIfSlugExists(slugToFind);
|
||||
});
|
||||
};
|
||||
return checkIfSlugExists(slugToFind);
|
||||
});
|
||||
};
|
||||
|
||||
slug = base.trim();
|
||||
|
||||
@ -248,12 +251,12 @@ ghostBookshelf.Model = ghostBookshelf.Model.extend({
|
||||
slug = slug.charAt(slug.length - 1) === '-' ? slug.substr(0, slug.length - 1) : slug;
|
||||
|
||||
// Check the filtered slug doesn't match any of the reserved keywords
|
||||
slug = /^(ghost|ghost\-admin|admin|wp\-admin|wp\-login|dashboard|logout|login|signin|signup|signout|register|archive|archives|category|categories|tag|tags|page|pages|post|posts|user|users|rss)$/g
|
||||
.test(slug) ? slug + '-post' : slug;
|
||||
slug = /^(ghost|ghost\-admin|admin|wp\-admin|wp\-login|dashboard|logout|login|signin|signup|signout|register|archive|archives|category|categories|tag|tags|page|pages|post|posts|user|users|rss|feed)$/g
|
||||
.test(slug) ? slug + '-' + baseName : slug;
|
||||
|
||||
//if slug is empty after trimming use "post"
|
||||
if (!slug) {
|
||||
slug = 'post';
|
||||
slug = baseName;
|
||||
}
|
||||
// Test for duplicate slugs.
|
||||
return checkIfSlugExists(slug);
|
||||
|
@ -1,5 +1,6 @@
|
||||
var migrations = require('../data/migration'),
|
||||
_ = require('lodash');
|
||||
_ = require('lodash'),
|
||||
when = require('when');
|
||||
|
||||
module.exports = {
|
||||
Post: require('./post').Post,
|
||||
@ -20,14 +21,14 @@ module.exports = {
|
||||
var self = this;
|
||||
|
||||
return self.Post.browse().then(function (posts) {
|
||||
_.each(posts.toJSON(), function (post) {
|
||||
self.Post.destroy(post.id);
|
||||
});
|
||||
return when.all(_.map(posts.toJSON(), function (post) {
|
||||
return self.Post.destroy(post.id);
|
||||
}));
|
||||
}).then(function () {
|
||||
self.Tag.browse().then(function (tags) {
|
||||
_.each(tags.toJSON(), function (tag) {
|
||||
self.Tag.destroy(tag.id);
|
||||
});
|
||||
return self.Tag.browse().then(function (tags) {
|
||||
return when.all(_.map(tags.toJSON(), function (tag) {
|
||||
return self.Tag.destroy(tag.id);
|
||||
}));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
@ -3,14 +3,14 @@ var _ = require('lodash'),
|
||||
when = require('when'),
|
||||
errors = require('../errorHandling'),
|
||||
Showdown = require('showdown'),
|
||||
github = require('../../shared/lib/showdown/extensions/github'),
|
||||
typography = require('../../shared/lib/showdown/extensions/typography'),
|
||||
converter = new Showdown.converter({extensions: [typography, github]}),
|
||||
ghostgfm = require('../../shared/lib/showdown/extensions/ghostgfm'),
|
||||
converter = new Showdown.converter({extensions: [ghostgfm]}),
|
||||
User = require('./user').User,
|
||||
Tag = require('./tag').Tag,
|
||||
Tags = require('./tag').Tags,
|
||||
ghostBookshelf = require('./base'),
|
||||
validation = require('../data/validation'),
|
||||
xmlrpc = require('../xmlrpc'),
|
||||
|
||||
Post,
|
||||
Posts;
|
||||
@ -29,7 +29,12 @@ Post = ghostBookshelf.Model.extend({
|
||||
initialize: function () {
|
||||
var self = this;
|
||||
this.on('creating', this.creating, this);
|
||||
this.on('saved', this.updateTags, this);
|
||||
this.on('saved', function (model, attributes, options) {
|
||||
if (model.get('status') === 'published') {
|
||||
xmlrpc.ping(model.attributes);
|
||||
}
|
||||
return self.updateTags(model, attributes, options);
|
||||
});
|
||||
this.on('saving', function (model, attributes, options) {
|
||||
return when(self.saving(model, attributes, options)).then(function () {
|
||||
return self.validate(model, attributes, options);
|
||||
@ -43,10 +48,21 @@ Post = ghostBookshelf.Model.extend({
|
||||
|
||||
saving: function (newPage, attr, options) {
|
||||
/*jshint unused:false*/
|
||||
var self = this;
|
||||
var self = this,
|
||||
tagsToCheck,
|
||||
i;
|
||||
|
||||
// keep tags for 'saved' event
|
||||
this.myTags = this.get('tags');
|
||||
// keep tags for 'saved' event and deduplicate upper/lowercase tags
|
||||
tagsToCheck = this.get('tags');
|
||||
this.myTags = [];
|
||||
_.each(tagsToCheck, function (item) {
|
||||
for (i = 0; i < self.myTags.length; i = i + 1) {
|
||||
if (self.myTags[i].name.toLocaleLowerCase() === item.name.toLocaleLowerCase()) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
self.myTags.push(item);
|
||||
});
|
||||
|
||||
ghostBookshelf.Model.prototype.saving.call(this);
|
||||
|
||||
|
@ -64,7 +64,9 @@ Settings = ghostBookshelf.Model.extend({
|
||||
if (!_.isObject(_key)) {
|
||||
_key = { key: _key };
|
||||
}
|
||||
return ghostBookshelf.Model.read.call(this, _key);
|
||||
return when(ghostBookshelf.Model.read.call(this, _key)).then(function (element) {
|
||||
return element;
|
||||
});
|
||||
},
|
||||
|
||||
edit: function (_data, t) {
|
||||
|
@ -8,13 +8,17 @@ Tag = ghostBookshelf.Model.extend({
|
||||
|
||||
tableName: 'tags',
|
||||
|
||||
saving: function () {
|
||||
saving: function (newPage, attr, options) {
|
||||
/*jshint unused:false*/
|
||||
|
||||
var self = this;
|
||||
|
||||
ghostBookshelf.Model.prototype.saving.apply(this, arguments);
|
||||
|
||||
if (!this.get('slug')) {
|
||||
// Generating a slug requires a db call to look for conflicting slugs
|
||||
return ghostBookshelf.Model.generateSlug(Tag, this.get('name'))
|
||||
if (this.hasChanged('slug') || !this.get('slug')) {
|
||||
// Pass the new slug through the generator to strip illegal characters, detect duplicates
|
||||
return ghostBookshelf.Model.generateSlug(Tag, this.get('slug') || this.get('name'),
|
||||
{transacting: options.transacting})
|
||||
.then(function (slug) {
|
||||
self.set({slug: slug});
|
||||
});
|
||||
|
@ -38,7 +38,9 @@ User = ghostBookshelf.Model.extend({
|
||||
|
||||
tableName: 'users',
|
||||
|
||||
saving: function () {
|
||||
saving: function (newPage, attr, options) {
|
||||
/*jshint unused:false*/
|
||||
|
||||
var self = this;
|
||||
// disabling sanitization until we can implement a better version
|
||||
// this.set('name', this.sanitize('name'));
|
||||
@ -49,14 +51,14 @@ User = ghostBookshelf.Model.extend({
|
||||
|
||||
ghostBookshelf.Model.prototype.saving.apply(this, arguments);
|
||||
|
||||
if (!this.get('slug')) {
|
||||
if (this.hasChanged('slug') || !this.get('slug')) {
|
||||
// Generating a slug requires a db call to look for conflicting slugs
|
||||
return ghostBookshelf.Model.generateSlug(User, this.get('name'))
|
||||
return ghostBookshelf.Model.generateSlug(User, this.get('slug') || this.get('name'),
|
||||
{transacting: options.transacting})
|
||||
.then(function (slug) {
|
||||
self.set({slug: slug});
|
||||
});
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
posts: function () {
|
||||
@ -175,7 +177,7 @@ User = ghostBookshelf.Model.extend({
|
||||
});
|
||||
}
|
||||
|
||||
return when(user.set('status', 'active').save()).then(function (user) {
|
||||
return when(user.set({status : 'active', last_login : new Date()}).save()).then(function (user) {
|
||||
return user;
|
||||
});
|
||||
}, errors.logAndThrowError);
|
||||
|
@ -16,7 +16,10 @@ var _ = require('lodash'),
|
||||
|
||||
fs.readFile(path, function (error, data) {
|
||||
if (error) {
|
||||
messages.errors.push({message: 'Could not read package.json file', context: path});
|
||||
messages.errors.push({
|
||||
message: 'Could not read package.json file',
|
||||
context: path
|
||||
});
|
||||
packageDeferred.resolve(false);
|
||||
return;
|
||||
}
|
||||
@ -25,11 +28,19 @@ var _ = require('lodash'),
|
||||
if (jsonContainer.hasOwnProperty('name') && jsonContainer.hasOwnProperty('version')) {
|
||||
packageDeferred.resolve(jsonContainer);
|
||||
} else {
|
||||
messages.errors.push({message: '"name" or "version" is missing from theme package.json file.', context: path});
|
||||
messages.errors.push({
|
||||
message: '"name" or "version" is missing from theme package.json file.',
|
||||
context: path,
|
||||
help: 'This will be required in future. Please see http://docs.ghost.org/themes/'
|
||||
});
|
||||
packageDeferred.resolve(false);
|
||||
}
|
||||
} catch (e) {
|
||||
messages.errors.push({message: 'Theme package.json file is malformed', context: path});
|
||||
messages.errors.push({
|
||||
message: 'Theme package.json file is malformed',
|
||||
context: path,
|
||||
help: 'This will be required in future. Please see http://docs.ghost.org/themes/'
|
||||
});
|
||||
packageDeferred.resolve(false);
|
||||
}
|
||||
});
|
||||
@ -70,7 +81,7 @@ var _ = require('lodash'),
|
||||
/*jslint unparam:true*/
|
||||
if (result.isDirectory()) {
|
||||
fileDeferred.resolve(readDir(fpath, options, depth + 1, messages));
|
||||
} else if (depth === 1 && file === "package.json") {
|
||||
} else if (depth === 1 && file === 'package.json') {
|
||||
fileDeferred.resolve(parsePackageJson(fpath, messages));
|
||||
} else {
|
||||
fileDeferred.resolve(fpath);
|
||||
@ -96,15 +107,21 @@ var _ = require('lodash'),
|
||||
|
||||
return when(readDir(dir, options, depth, messages)).then(function (paths) {
|
||||
// for all contents of the dir, I'm interested in the ones that are directories and within /theme/
|
||||
if (typeof paths === "object" && dir.indexOf('theme') !== -1) {
|
||||
if (typeof paths === 'object' && dir.indexOf('theme') !== -1) {
|
||||
_.each(paths, function (path, index) {
|
||||
if (typeof path === 'object' && !path.hasOwnProperty('package.json') && index.indexOf('.') !== 0) {
|
||||
messages.warns.push({message: 'Theme does not have a package.json file', context: index});
|
||||
messages.warns.push({
|
||||
message: 'Found a theme with no package.json file',
|
||||
context: 'Theme name: ' + index,
|
||||
help: 'This will be required in future. Please see http://docs.ghost.org/themes/'
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
paths._messages = messages;
|
||||
return paths;
|
||||
}).otherwise(function () {
|
||||
return {'_messages': messages};
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -1,6 +1,9 @@
|
||||
var admin = require('../controllers/admin'),
|
||||
config = require('../config'),
|
||||
middleware = require('../middleware').middleware;
|
||||
middleware = require('../middleware').middleware,
|
||||
|
||||
ONE_HOUR_S = 60 * 60,
|
||||
ONE_YEAR_S = 365 * 24 * ONE_HOUR_S;
|
||||
|
||||
module.exports = function (server) {
|
||||
// Have ember route look for hits first
|
||||
@ -11,24 +14,24 @@ module.exports = function (server) {
|
||||
// ### Admin routes
|
||||
server.get('/logout/', function redirect(req, res) {
|
||||
/*jslint unparam:true*/
|
||||
res.set({'Cache-Control': 'public, max-age=' + ONE_YEAR_S});
|
||||
res.redirect(301, subdir + '/ghost/signout/');
|
||||
});
|
||||
server.get('/signout/', function redirect(req, res) {
|
||||
/*jslint unparam:true*/
|
||||
res.set({'Cache-Control': 'public, max-age=' + ONE_YEAR_S});
|
||||
res.redirect(301, subdir + '/ghost/signout/');
|
||||
});
|
||||
server.get('/signin/', function redirect(req, res) {
|
||||
/*jslint unparam:true*/
|
||||
res.set({'Cache-Control': 'public, max-age=' + ONE_YEAR_S});
|
||||
res.redirect(301, subdir + '/ghost/signin/');
|
||||
});
|
||||
server.get('/signup/', function redirect(req, res) {
|
||||
/*jslint unparam:true*/
|
||||
res.set({'Cache-Control': 'public, max-age=' + ONE_YEAR_S});
|
||||
res.redirect(301, subdir + '/ghost/signup/');
|
||||
});
|
||||
server.get('/ghost/login/', function redirect(req, res) {
|
||||
/*jslint unparam:true*/
|
||||
res.redirect(301, subdir + '/ghost/signin/');
|
||||
});
|
||||
|
||||
server.get('/ghost/signout/', admin.signout);
|
||||
server.get('/ghost/signin/', middleware.redirectToSignup, middleware.redirectToDashboard, admin.signin);
|
||||
|
@ -9,7 +9,7 @@ module.exports = function (server) {
|
||||
server.get('/ghost/api/v0.1/posts/:id', api.requestHandler(api.posts.read));
|
||||
server.put('/ghost/api/v0.1/posts/:id', api.requestHandler(api.posts.edit));
|
||||
server.del('/ghost/api/v0.1/posts/:id', api.requestHandler(api.posts.destroy));
|
||||
server.get('/ghost/api/v0.1/posts/getSlug/:title', middleware.authAPI, api.requestHandler(api.posts.getSlug));
|
||||
server.get('/ghost/api/v0.1/posts/slug/:title', middleware.authAPI, api.requestHandler(api.posts.generateSlug));
|
||||
// #### Settings
|
||||
server.get('/ghost/api/v0.1/settings/', api.requestHandler(api.settings.browse));
|
||||
server.get('/ghost/api/v0.1/settings/:key/', api.requestHandler(api.settings.read));
|
||||
|
@ -1,11 +1,22 @@
|
||||
var frontend = require('../controllers/frontend');
|
||||
var frontend = require('../controllers/frontend'),
|
||||
config = require('../config'),
|
||||
|
||||
ONE_HOUR_S = 60 * 60,
|
||||
ONE_YEAR_S = 365 * 24 * ONE_HOUR_S;
|
||||
|
||||
module.exports = function (server) {
|
||||
/*jslint regexp: true */
|
||||
var subdir = config().paths.subdir;
|
||||
|
||||
// ### Frontend routes
|
||||
server.get('/rss/', frontend.rss);
|
||||
server.get('/rss/:page/', frontend.rss);
|
||||
server.get('/feed/', function redirect(req, res) {
|
||||
/*jshint unused:true*/
|
||||
res.set({'Cache-Control': 'public, max-age=' + ONE_YEAR_S});
|
||||
res.redirect(301, subdir + '/rss/');
|
||||
});
|
||||
|
||||
|
||||
server.get('/tag/:slug/rss/', frontend.rss);
|
||||
server.get('/tag/:slug/rss/:page/', frontend.rss);
|
||||
server.get('/tag/:slug/page/:page/', frontend.tag);
|
||||
@ -13,4 +24,6 @@ module.exports = function (server) {
|
||||
server.get('/page/:page/', frontend.homepage);
|
||||
server.get('/', frontend.homepage);
|
||||
server.get('*', frontend.single);
|
||||
|
||||
|
||||
};
|
@ -41,7 +41,7 @@ baseStore = {
|
||||
'getUniqueFileName': function (store, image, targetDir) {
|
||||
var done = when.defer(),
|
||||
ext = path.extname(image.name),
|
||||
name = path.basename(image.name, ext).replace(/[\W]/gi, '_');
|
||||
name = path.basename(image.name, ext).replace(/[\W]/gi, '-');
|
||||
|
||||
this.generateUnique(store, targetDir, name, ext, 0, done);
|
||||
|
||||
|
@ -33,7 +33,7 @@ localFileStore = _.extend(baseStore, {
|
||||
}).then(function () {
|
||||
// The src for the image must be in URI format, not a file system path, which in Windows uses \
|
||||
// For local file system storage can use relative path so add a slash
|
||||
var fullUrl = (config().paths.subdir + '/' + path.relative(config().paths.appRoot, targetFilename)).replace(new RegExp('\\' + path.sep, 'g'), '/');
|
||||
var fullUrl = (config().paths.subdir + '/' + config().paths.imagesRelPath + '/' + path.relative(config().paths.imagesPath, targetFilename)).replace(new RegExp('\\' + path.sep, 'g'), '/');
|
||||
return saved.resolve(fullUrl);
|
||||
}).otherwise(function (e) {
|
||||
errors.logError(e);
|
||||
|
@ -1,4 +1,15 @@
|
||||
{{!< default}}
|
||||
<style>
|
||||
/* Additional styling for touch editor, will be moved in future */
|
||||
.touch-editor #entry-markdown {
|
||||
padding: 15px;
|
||||
margin-bottom: 40px;
|
||||
font-family: monospace;
|
||||
font-size: 1.4em;
|
||||
line-height: 1.3em;
|
||||
color: #242628;
|
||||
}
|
||||
</style>
|
||||
<section class="entry-container">
|
||||
<header>
|
||||
<section class="box entry-title">
|
||||
|
65
core/server/xmlrpc.js
Normal file
65
core/server/xmlrpc.js
Normal file
@ -0,0 +1,65 @@
|
||||
var _ = require('lodash'),
|
||||
config = require('./config'),
|
||||
errors = require('./errorHandling'),
|
||||
http = require('http'),
|
||||
xml = require('xml'),
|
||||
pingList;
|
||||
|
||||
// ToDo: Make this configurable
|
||||
pingList = [
|
||||
{ host: 'blogsearch.google.com', path: '/ping/RPC2' },
|
||||
{ host: 'rpc.pingomatic.com', path: '/' }
|
||||
];
|
||||
|
||||
function ping(post) {
|
||||
var pingXML,
|
||||
title = post.title;
|
||||
|
||||
// Only ping when in production and not a page
|
||||
if (process.env.NODE_ENV !== 'production' || post.page) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Need to require here because of circular dependency
|
||||
return config.urlForPost(require('./api').settings, post, true).then(function (url) {
|
||||
|
||||
// Build XML object.
|
||||
pingXML = xml({
|
||||
methodCall: [
|
||||
{ methodName: 'weblogUpdate.ping' },
|
||||
{
|
||||
params: [{
|
||||
param: [{ value: [{ string: title }]}],
|
||||
}, {
|
||||
param: [{ value: [{ string: url }]}],
|
||||
}]
|
||||
}
|
||||
]
|
||||
}, {declaration: true});
|
||||
|
||||
// Ping each of the defined services.
|
||||
_.each(pingList, function (pingHost) {
|
||||
var options = {
|
||||
hostname: pingHost.host,
|
||||
path: pingHost.path,
|
||||
method: 'POST'
|
||||
},
|
||||
req;
|
||||
|
||||
req = http.request(options);
|
||||
req.write(pingXML);
|
||||
req.on('error', function (error) {
|
||||
errors.logError(
|
||||
error,
|
||||
"Pinging services for updates on your blog failed, your blog will continue to function.",
|
||||
"If you get this error repeatedly, please seek help from https://ghost.org/forum."
|
||||
);
|
||||
});
|
||||
req.end();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
ping: ping
|
||||
};
|
@ -1,10 +1,16 @@
|
||||
/* jshint node:true, browser:true */
|
||||
|
||||
// Ghost GFM
|
||||
// Taken and extended from the Showdown Github Extension (WIP)
|
||||
// Makes a number of pre and post-processing changes to the way markdown is handled
|
||||
//
|
||||
// Github Extension (WIP)
|
||||
// ~~strike-through~~ -> <del>strike-through</del>
|
||||
//
|
||||
// ~~strike-through~~ -> <del>strike-through</del> (Pre)
|
||||
// GFM newlines & underscores (Pre)
|
||||
// 4 or more underscores (Pre)
|
||||
// autolinking / custom image handling (Post)
|
||||
|
||||
(function () {
|
||||
var github = function (converter) {
|
||||
var ghostgfm = function () {
|
||||
return [
|
||||
{
|
||||
// strike-through
|
||||
@ -73,6 +79,17 @@
|
||||
return text;
|
||||
}
|
||||
},
|
||||
|
||||
// 4 or more inline underscores e.g. Ghost rocks my _____!
|
||||
{
|
||||
type: 'lang',
|
||||
filter: function (text) {
|
||||
return text.replace(/([^_\n\r])(_{4,})/g, function (match, prefix, underscores) {
|
||||
return prefix + underscores.replace(/_/g, '_');
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
// GFM autolinking & custom image handling, happens AFTER showdown
|
||||
type : 'html',
|
||||
@ -131,7 +148,11 @@
|
||||
};
|
||||
|
||||
// Client-side export
|
||||
if (typeof window !== 'undefined' && window.Showdown && window.Showdown.extensions) { window.Showdown.extensions.github = github; }
|
||||
if (typeof window !== 'undefined' && window.Showdown && window.Showdown.extensions) {
|
||||
window.Showdown.extensions.ghostgfm = ghostgfm;
|
||||
}
|
||||
// Server-side export
|
||||
if (typeof module !== 'undefined') module.exports = github;
|
||||
if (typeof module !== 'undefined') {
|
||||
module.exports = ghostgfm;
|
||||
}
|
||||
}());
|
@ -1,6 +1,15 @@
|
||||
/* jshint node:true, browser:true */
|
||||
|
||||
// Ghost Image Preview
|
||||
//
|
||||
// Manages the conversion of image markdown `![]()` from markdown into the HTML image preview
|
||||
// This provides a dropzone and other interface elements for adding images
|
||||
// Is only used in the admin client.
|
||||
|
||||
|
||||
var Ghost = Ghost || {};
|
||||
(function () {
|
||||
var ghostdown = function () {
|
||||
var ghostimagepreview = function () {
|
||||
return [
|
||||
// ![] image syntax
|
||||
{
|
||||
@ -12,25 +21,24 @@
|
||||
pathRegex = /^(\/)?([^\/\0]+(\/)?)+$/i;
|
||||
|
||||
return text.replace(imageMarkdownRegex, function (match, key, alt, src) {
|
||||
var result = "";
|
||||
var result = '',
|
||||
output;
|
||||
|
||||
if (src && (src.match(uriRegex) || src.match(pathRegex))) {
|
||||
result = '<img class="js-upload-target" src="' + src + '"/>';
|
||||
}
|
||||
return '<section id="image_upload_' + key + '" class="js-drop-zone image-uploader">' + result +
|
||||
'<div class="description">Add image of <strong>' + alt + '</strong></div>' +
|
||||
'<input data-url="upload" class="js-fileupload main fileupload" type="file" name="uploadimage">' +
|
||||
'</section>';
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// 4 or more inline underscores e.g. Ghost rocks my _____!
|
||||
{
|
||||
type: 'lang',
|
||||
filter: function (text) {
|
||||
return text.replace(/([^_\n\r])(_{4,})/g, function (match, prefix, underscores) {
|
||||
return prefix + underscores.replace(/_/g, '_');
|
||||
if (Ghost && Ghost.touchEditor) {
|
||||
output = '<section class="image-uploader">' +
|
||||
result + '<div class="description">Mobile uploads coming soon</div></section>';
|
||||
} else {
|
||||
output = '<section id="image_upload_' + key + '" class="js-drop-zone image-uploader">' +
|
||||
result + '<div class="description">Add image of <strong>' + alt + '</strong></div>' +
|
||||
'<input data-url="upload" class="js-fileupload main fileupload" type="file" name="uploadimage">' +
|
||||
'</section>';
|
||||
}
|
||||
|
||||
return output;
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -39,10 +47,10 @@
|
||||
|
||||
// Client-side export
|
||||
if (typeof window !== 'undefined' && window.Showdown && window.Showdown.extensions) {
|
||||
window.Showdown.extensions.ghostdown = ghostdown;
|
||||
window.Showdown.extensions.ghostimagepreview = ghostimagepreview;
|
||||
}
|
||||
// Server-side export
|
||||
if (typeof module !== 'undefined') {
|
||||
module.exports = ghostdown;
|
||||
module.exports = ghostimagepreview;
|
||||
}
|
||||
}());
|
@ -1,114 +0,0 @@
|
||||
/*global module */
|
||||
//
|
||||
// Replaces straight quotes with curly ones, -- and --- with en dash and em
|
||||
// dash respectively, and ... with horizontal ellipses.
|
||||
//
|
||||
|
||||
(function () {
|
||||
var typography = function () {
|
||||
return [
|
||||
{
|
||||
type: "lang",
|
||||
filter: function (text) {
|
||||
var fCodeblocks = {}, nCodeblocks = {}, iCodeblocks = {},
|
||||
e = {
|
||||
endash: '\u2009\u2013\u2009', // U+2009 = thin space
|
||||
emdash: '\u2014',
|
||||
lsquo: '\u2018',
|
||||
rsquo: '\u2019',
|
||||
ldquo: '\u201c',
|
||||
rdquo: '\u201d',
|
||||
hellip: '\u2026'
|
||||
},
|
||||
|
||||
i;
|
||||
|
||||
// Extract fenced code blocks.
|
||||
i = -1;
|
||||
text = text.replace(/```((?:.|\n)+?)```/g,
|
||||
function (match, code) {
|
||||
i += 1;
|
||||
fCodeblocks[i] = "```" + code + "```";
|
||||
return "{typog-fcb-" + i + "}";
|
||||
});
|
||||
|
||||
// Extract indented code blocks.
|
||||
i = -1;
|
||||
text = text.replace(/((\n+([ ]{4}|\t).+)+)/g,
|
||||
function (match, code) {
|
||||
i += 1;
|
||||
nCodeblocks[i] = " " + code;
|
||||
return "{typog-ncb-" + i + "}";
|
||||
});
|
||||
|
||||
// Extract inline code blocks
|
||||
i = -1;
|
||||
text = text.replace(/`(.+)`/g, function (match, code) {
|
||||
i += 1;
|
||||
iCodeblocks[i] = "`" + code + "`";
|
||||
return "{typog-icb-" + i + "}";
|
||||
});
|
||||
|
||||
// Perform typographic symbol replacement.
|
||||
|
||||
// Double quotes. There might be a reason this doesn't use
|
||||
// the same \b matching style as the single quotes, but I
|
||||
// can't remember what it is :(
|
||||
text = text.
|
||||
// Opening quotes
|
||||
replace(/"([\w'])/g, e.ldquo + "$1").
|
||||
// All the rest
|
||||
replace(/"/g, e.rdquo);
|
||||
|
||||
// Single quotes/apostrophes
|
||||
text = text.
|
||||
// Apostrophes first
|
||||
replace(/\b'\b/g, e.rsquo).
|
||||
// Opening quotes
|
||||
replace(/'\b/g, e.lsquo).
|
||||
// All the rest
|
||||
replace(/'/g, e.rsquo);
|
||||
|
||||
// Dashes
|
||||
text = text.
|
||||
// Don't replace lines containing only hyphens
|
||||
replace(/^-+$/gm, "{typog-hr}").
|
||||
replace(/---/g, e.emdash).
|
||||
replace(/ -- /g, e.endash).
|
||||
replace(/{typog-hr}/g, "----");
|
||||
|
||||
// Ellipses.
|
||||
text = text.replace(/\.{3}/g, e.hellip);
|
||||
|
||||
|
||||
// Restore fenced code blocks.
|
||||
text = text.replace(/{typog-fcb-([0-9]+)}/g, function (x, y) {
|
||||
return fCodeblocks[y];
|
||||
});
|
||||
|
||||
// Restore indented code blocks.
|
||||
text = text.replace(/{typog-ncb-([0-9]+)}/g, function (x, y) {
|
||||
return nCodeblocks[y];
|
||||
});
|
||||
|
||||
// Restore inline code blocks.
|
||||
text = text.replace(/{typog-icb-([0-9]+)}/g, function (x, y) {
|
||||
return iCodeblocks[y];
|
||||
});
|
||||
|
||||
return text;
|
||||
}
|
||||
}
|
||||
];
|
||||
};
|
||||
|
||||
// Client-side export
|
||||
if (typeof window !== 'undefined' && window.Showdown && window.Showdown.extensions) {
|
||||
window.Showdown.extensions.typography = typography;
|
||||
}
|
||||
// Server-side export
|
||||
if (typeof module !== 'undefined') {
|
||||
module.exports = typography;
|
||||
}
|
||||
}());
|
||||
|
@ -1,6 +1,6 @@
|
||||
/*globals casper, __utils__, url, testPost */
|
||||
|
||||
CasperTest.begin("Content screen is correct", 20, function suite(test) {
|
||||
CasperTest.begin("Content screen is correct", 12, function suite(test) {
|
||||
// Create a sample post
|
||||
casper.thenOpen(url + 'ghost/editor/', function testTitleAndUrl() {
|
||||
test.assertTitle('Ghost Admin', 'Ghost admin has no title');
|
||||
@ -27,18 +27,6 @@ CasperTest.begin("Content screen is correct", 20, function suite(test) {
|
||||
test.assertUrlMatch(/ghost\/content\/$/, "Ghost doesn't require login this time");
|
||||
});
|
||||
|
||||
casper.then(function testMenus() {
|
||||
test.assertExists("#main-menu", "Main menu is present");
|
||||
test.assertSelectorHasText("#main-menu .content a", "Content");
|
||||
test.assertSelectorHasText("#main-menu .editor a", "New Post");
|
||||
test.assertSelectorHasText("#main-menu .settings a", "Settings");
|
||||
|
||||
test.assertExists("#usermenu", "User menu is present");
|
||||
test.assertSelectorHasText("#usermenu .usermenu-profile a", "Your Profile");
|
||||
test.assertSelectorHasText("#usermenu .usermenu-help a", "Help / Support");
|
||||
test.assertSelectorHasText("#usermenu .usermenu-signout a", "Sign Out");
|
||||
});
|
||||
|
||||
casper.then(function testViews() {
|
||||
test.assertExists(".content-view-container", "Content main view is present");
|
||||
test.assertExists(".content-list-content", "Content list view is present");
|
||||
@ -129,4 +117,59 @@ CasperTest.begin("Posts can be marked as featured", 12, function suite(test) {
|
||||
}, function onTimeout() {
|
||||
test.assert(false, 'Success notification wont go away:(');
|
||||
});
|
||||
});
|
||||
|
||||
CasperTest.begin('Admin navigation bar is correct', 28, function suite(test) {
|
||||
casper.thenOpen(url + 'ghost/content/', function testTitleAndUrl() {
|
||||
test.assertTitle('Ghost Admin', 'Ghost admin has no title');
|
||||
test.assertUrlMatch(/ghost\/content\/$/, "Ghost doesn't require login this time");
|
||||
});
|
||||
|
||||
casper.then(function testNavItems() {
|
||||
test.assertExists('a.ghost-logo', 'Ghost logo home page link exists');
|
||||
test.assertEquals(this.getElementAttribute('a.ghost-logo', 'href'), '/', 'Ghost logo href is correct');
|
||||
|
||||
test.assertExists('#main-menu li.content a', 'Content nav item exists');
|
||||
test.assertSelectorHasText('#main-menu li.content a', 'Content', 'Content nav item has correct text');
|
||||
test.assertEquals(this.getElementAttribute('#main-menu li.content a', 'href'), '/ghost/', 'Content href is correct');
|
||||
test.assertEval(function testContentIsNotActive() {
|
||||
return document.querySelector('#main-menu li.content').classList.contains('active');
|
||||
}, 'Content nav item is marked active');
|
||||
|
||||
test.assertExists('#main-menu li.editor a', 'Editor nav item exists');
|
||||
test.assertSelectorHasText('#main-menu li.editor a', 'New Post', 'Editor nav item has correct text');
|
||||
test.assertEquals(this.getElementAttribute('#main-menu li.editor a', 'href'), '/ghost/editor/', 'Editor href is correct');
|
||||
test.assertEval(function testEditorIsNotActive() {
|
||||
return !document.querySelector('#main-menu li.editor').classList.contains('active');
|
||||
}, 'Editor nav item is not marked active');
|
||||
|
||||
test.assertExists('#main-menu li.settings a', 'Settings nav item exists');
|
||||
test.assertSelectorHasText('#main-menu li.settings a', 'Settings', 'Settings nav item has correct text');
|
||||
test.assertEquals(this.getElementAttribute('#main-menu li.settings a', 'href'), '/ghost/settings/', 'Settings href is correct');
|
||||
test.assertEval(function testSettingsIsActive() {
|
||||
return !document.querySelector('#main-menu li.settings').classList.contains('active');
|
||||
}, 'Settings nav item is not marked active');
|
||||
});
|
||||
|
||||
casper.then(function testUserMenuNotVisible() {
|
||||
test.assertExists('#usermenu', 'User menu nav item exists');
|
||||
test.assertNotVisible('#usermenu ul.overlay', 'User menu should not be visible');
|
||||
});
|
||||
|
||||
casper.thenClick('#usermenu a');
|
||||
casper.waitForSelector('#usermenu ul.overlay', function then() {
|
||||
test.assertVisible('#usermenu ul.overlay', 'User menu should be visible');
|
||||
|
||||
test.assertExists('#usermenu li.usermenu-profile a', 'Profile menu item exists');
|
||||
test.assertSelectorHasText('#usermenu li.usermenu-profile a', 'Your Profile', 'Profile menu item has correct text');
|
||||
test.assertEquals(this.getElementAttribute('li.usermenu-profile a', 'href'), '/ghost/settings/user/', 'Profile href is correct');
|
||||
|
||||
test.assertExists('#usermenu li.usermenu-help a', 'Help menu item exists');
|
||||
test.assertSelectorHasText('#usermenu li.usermenu-help a', 'Help / Support', 'Help menu item has correct text');
|
||||
test.assertEquals(this.getElementAttribute('li.usermenu-help a', 'href'), 'http://ghost.org/forum/', 'Help href is correct');
|
||||
|
||||
test.assertExists('#usermenu li.usermenu-signout a', 'Sign Out menu item exists');
|
||||
test.assertSelectorHasText('#usermenu li.usermenu-signout a', 'Sign Out', 'Sign Out menu item has correct text');
|
||||
test.assertEquals(this.getElementAttribute('#usermenu li.usermenu-signout a', 'href'), '/ghost/signout/', 'Sign Out href is correct');
|
||||
});
|
||||
});
|
@ -228,13 +228,12 @@ CasperTest.begin("Tag editor", 6, function suite(test) {
|
||||
|
||||
casper.thenClick(createdTagSelector);
|
||||
|
||||
casper.then(function () {
|
||||
test.assertDoesntExist(createdTagSelector, "clicking the tag should delete the tag");
|
||||
casper.waitWhileSelector(createdTagSelector, function onSuccess() {
|
||||
test.assert(true, "clicking the tag should delete the tag");
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
CasperTest.begin("Post settings menu", 17, function suite(test) {
|
||||
CasperTest.begin("Post settings menu", 18, function suite(test) {
|
||||
casper.thenOpen(url + "ghost/editor/", function testTitleAndUrl() {
|
||||
test.assertTitle("Ghost Admin", "Ghost admin has no title");
|
||||
});
|
||||
@ -271,7 +270,11 @@ CasperTest.begin("Post settings menu", 17, function suite(test) {
|
||||
casper.thenClick("#publish-bar a.post-settings");
|
||||
|
||||
casper.waitUntilVisible("#publish-bar .post-settings-menu", function onSuccess() {
|
||||
test.assertVisible(".post-settings-menu a.delete", "delete post button should be visible for saved drafts");
|
||||
test.assert(true, "post settings menu should be visible after clicking post-settings icon");
|
||||
});
|
||||
|
||||
casper.waitUntilVisible(".post-settings-menu a.delete", function onSuccess() {
|
||||
test.assert(true, "delete post button should be visible for saved drafts");
|
||||
});
|
||||
|
||||
// Test Static Page conversion
|
||||
@ -310,7 +313,9 @@ CasperTest.begin("Post settings menu", 17, function suite(test) {
|
||||
|
||||
casper.thenClick("#publish-bar a.post-settings");
|
||||
casper.thenClick(".post-settings-menu a.delete");
|
||||
casper.thenClick("#modal-container .js-button-accept");
|
||||
casper.waitUntilVisible("#modal-container", function onSuccess() {
|
||||
casper.thenClick("#modal-container .js-button-accept");
|
||||
});
|
||||
|
||||
casper.waitForUrl(/ghost\/content\/$/, function onSuccess() {
|
||||
test.assert(true, "clicking the delete post button should bring us to the content page");
|
||||
@ -446,3 +451,58 @@ CasperTest.begin('Publish menu - existing post', 22, function suite(test) {
|
||||
test.assert(false, 'Publish split button should have .splitbutton-delete');
|
||||
});
|
||||
});
|
||||
|
||||
CasperTest.begin('Admin navigation bar is correct', 28, function suite(test) {
|
||||
casper.thenOpen(url + 'ghost/editor/', function testTitleAndUrl() {
|
||||
test.assertTitle('Ghost Admin', 'Ghost admin has no title');
|
||||
test.assertUrlMatch(/ghost\/editor\/$/, "Ghost doesn't require login this time");
|
||||
});
|
||||
|
||||
casper.then(function testNavItems() {
|
||||
test.assertExists('a.ghost-logo', 'Ghost logo home page link exists');
|
||||
test.assertEquals(this.getElementAttribute('a.ghost-logo', 'href'), '/', 'Ghost logo href is correct');
|
||||
|
||||
test.assertExists('#main-menu li.content a', 'Content nav item exists');
|
||||
test.assertSelectorHasText('#main-menu li.content a', 'Content', 'Content nav item has correct text');
|
||||
test.assertEquals(this.getElementAttribute('#main-menu li.content a', 'href'), '/ghost/', 'Content href is correct');
|
||||
test.assertEval(function testContentIsNotActive() {
|
||||
return !document.querySelector('#main-menu li.content').classList.contains('active');
|
||||
}, 'Content nav item is not marked active');
|
||||
|
||||
test.assertExists('#main-menu li.editor a', 'Editor nav item exists');
|
||||
test.assertSelectorHasText('#main-menu li.editor a', 'New Post', 'Editor nav item has correct text');
|
||||
test.assertEquals(this.getElementAttribute('#main-menu li.editor a', 'href'), '/ghost/editor/', 'Editor href is correct');
|
||||
test.assertEval(function testEditorIsNotActive() {
|
||||
return document.querySelector('#main-menu li.editor').classList.contains('active');
|
||||
}, 'Editor nav item is marked active');
|
||||
|
||||
test.assertExists('#main-menu li.settings a', 'Settings nav item exists');
|
||||
test.assertSelectorHasText('#main-menu li.settings a', 'Settings', 'Settings nav item has correct text');
|
||||
test.assertEquals(this.getElementAttribute('#main-menu li.settings a', 'href'), '/ghost/settings/', 'Settings href is correct');
|
||||
test.assertEval(function testSettingsIsActive() {
|
||||
return !document.querySelector('#main-menu li.settings').classList.contains('active');
|
||||
}, 'Settings nav item is not marked active');
|
||||
});
|
||||
|
||||
casper.then(function testUserMenuNotVisible() {
|
||||
test.assertExists('#usermenu', 'User menu nav item exists');
|
||||
test.assertNotVisible('#usermenu ul.overlay', 'User menu should not be visible');
|
||||
});
|
||||
|
||||
casper.thenClick('#usermenu a');
|
||||
casper.waitForSelector('#usermenu ul.overlay', function then() {
|
||||
test.assertVisible('#usermenu ul.overlay', 'User menu should be visible');
|
||||
|
||||
test.assertExists('#usermenu li.usermenu-profile a', 'Profile menu item exists');
|
||||
test.assertSelectorHasText('#usermenu li.usermenu-profile a', 'Your Profile', 'Profile menu item has correct text');
|
||||
test.assertEquals(this.getElementAttribute('li.usermenu-profile a', 'href'), '/ghost/settings/user/', 'Profile href is correct');
|
||||
|
||||
test.assertExists('#usermenu li.usermenu-help a', 'Help menu item exists');
|
||||
test.assertSelectorHasText('#usermenu li.usermenu-help a', 'Help / Support', 'Help menu item has correct text');
|
||||
test.assertEquals(this.getElementAttribute('#usermenu li.usermenu-help a', 'href'), 'http://ghost.org/forum/', 'Help href is correct');
|
||||
|
||||
test.assertExists('#usermenu li.usermenu-signout a', 'Sign Out menu item exists');
|
||||
test.assertSelectorHasText('#usermenu li.usermenu-signout a', 'Sign Out', 'Sign Out menu item has correct text');
|
||||
test.assertEquals(this.getElementAttribute('#usermenu li.usermenu-signout a', 'href'), '/ghost/signout/', 'Sign Out href is correct');
|
||||
});
|
||||
});
|
||||
|
@ -113,7 +113,7 @@ CasperTest.begin("Login limit is in place", 3, function suite(test) {
|
||||
}, true);
|
||||
|
||||
CasperTest.begin("Can login to Ghost", 4, function suite(test) {
|
||||
casper.thenOpen(url + "ghost/login/", function testTitle() {
|
||||
casper.thenOpen(url + "ghost/signin/", function testTitle() {
|
||||
test.assertTitle("Ghost Admin", "Ghost admin has no title");
|
||||
});
|
||||
|
||||
|
@ -1,5 +1,7 @@
|
||||
/*globals casper, __utils__, url */
|
||||
|
||||
// var navTest = require('../partials/partial_test');
|
||||
|
||||
CasperTest.begin("Settings screen is correct", 18, function suite(test) {
|
||||
casper.thenOpen(url + "ghost/settings/", function testTitleAndUrl() {
|
||||
test.assertTitle("Ghost Admin", "Ghost admin has no title");
|
||||
@ -388,3 +390,58 @@ CasperTest.begin('Ensure user location field length validation', 3, function sui
|
||||
test.fail('Location field length error did not appear');
|
||||
}, 2000);
|
||||
});
|
||||
|
||||
CasperTest.begin('Admin navigation bar is correct', 28, function suite(test) {
|
||||
casper.thenOpen(url + 'ghost/settings/', function testTitleAndUrl() {
|
||||
test.assertTitle('Ghost Admin', 'Ghost admin has no title');
|
||||
test.assertUrlMatch(/ghost\/settings\/general\/$/, "Ghost doesn't require login this time");
|
||||
});
|
||||
|
||||
casper.then(function testNavItems() {
|
||||
test.assertExists('a.ghost-logo', 'Ghost logo home page link exists');
|
||||
test.assertEquals(this.getElementAttribute('a.ghost-logo', 'href'), '/', 'Ghost logo href is correct');
|
||||
|
||||
test.assertExists('#main-menu li.content a', 'Content nav item exists');
|
||||
test.assertSelectorHasText('#main-menu li.content a', 'Content', 'Content nav item has correct text');
|
||||
test.assertEquals(this.getElementAttribute('#main-menu li.content a', 'href'), '/ghost/', 'Content href is correct');
|
||||
test.assertEval(function testContentIsNotActive() {
|
||||
return !document.querySelector('#main-menu li.content').classList.contains('active');
|
||||
}, 'Content nav item is not marked active');
|
||||
|
||||
test.assertExists('#main-menu li.editor a', 'Editor nav item exists');
|
||||
test.assertSelectorHasText('#main-menu li.editor a', 'New Post', 'Editor nav item has correct text');
|
||||
test.assertEquals(this.getElementAttribute('#main-menu li.editor a', 'href'), '/ghost/editor/', 'Editor href is correct');
|
||||
test.assertEval(function testEditorIsNotActive() {
|
||||
return !document.querySelector('#main-menu li.editor').classList.contains('active');
|
||||
}, 'Editor nav item is not marked active');
|
||||
|
||||
test.assertExists('#main-menu li.settings a', 'Settings nav item exists');
|
||||
test.assertSelectorHasText('#main-menu li.settings a', 'Settings', 'Settings nav item has correct text');
|
||||
test.assertEquals(this.getElementAttribute('#main-menu li.settings a', 'href'), '/ghost/settings/', 'Settings href is correct');
|
||||
test.assertEval(function testSettingsIsActive() {
|
||||
return document.querySelector('#main-menu li.settings').classList.contains('active');
|
||||
}, 'Settings nav item is marked active');
|
||||
});
|
||||
|
||||
casper.then(function testUserMenuNotVisible() {
|
||||
test.assertExists('#usermenu', 'User menu nav item exists');
|
||||
test.assertNotVisible('#usermenu ul.overlay', 'User menu should not be visible');
|
||||
});
|
||||
|
||||
casper.thenClick('#usermenu a');
|
||||
casper.waitForSelector('#usermenu ul.overlay', function then() {
|
||||
test.assertVisible('#usermenu ul.overlay', 'User menu should be visible');
|
||||
|
||||
test.assertExists('#usermenu li.usermenu-profile a', 'Profile menu item exists');
|
||||
test.assertSelectorHasText('#usermenu li.usermenu-profile a', 'Your Profile', 'Profile menu item has correct text');
|
||||
test.assertEquals(this.getElementAttribute('#usermenu li.usermenu-profile a', 'href'), '/ghost/settings/user/', 'Profile href is correct');
|
||||
|
||||
test.assertExists('#usermenu li.usermenu-help a', 'Help menu item exists');
|
||||
test.assertSelectorHasText('#usermenu li.usermenu-help a', 'Help / Support', 'Help menu item has correct text');
|
||||
test.assertEquals(this.getElementAttribute('#usermenu li.usermenu-help a', 'href'), 'http://ghost.org/forum/', 'Help href is correct');
|
||||
|
||||
test.assertExists('#usermenu li.usermenu-signout a', 'Sign Out menu item exists');
|
||||
test.assertSelectorHasText('#usermenu li.usermenu-signout a', 'Sign Out', 'Sign Out menu item has correct text');
|
||||
test.assertEquals(this.getElementAttribute('#usermenu li.usermenu-signout a', 'href'), '/ghost/signout/', 'Sign Out href is correct');
|
||||
});
|
||||
});
|
||||
|
@ -1,20 +0,0 @@
|
||||
/*globals describe, before, after, beforeEach, afterEach, it */
|
||||
var testUtils = require('../../utils'),
|
||||
should = require('should'),
|
||||
_ = require('lodash'),
|
||||
request = require('request');
|
||||
|
||||
describe('Unauthorized', function () {
|
||||
|
||||
it('can\'t retrieve posts', function (done) {
|
||||
request.get(testUtils.API.getApiURL('posts/'), function (error, response, body) {
|
||||
response.should.have.status(401);
|
||||
should.not.exist(response.headers['x-cache-invalidate']);
|
||||
response.should.be.json;
|
||||
var jsonResponse = JSON.parse(body);
|
||||
jsonResponse.should.exist;
|
||||
testUtils.API.checkResponseValue(jsonResponse, ['error']);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
@ -1,496 +0,0 @@
|
||||
/*globals describe, before, after, beforeEach, afterEach, it */
|
||||
var testUtils = require('../../utils'),
|
||||
should = require('should'),
|
||||
_ = require('lodash'),
|
||||
request = require('request');
|
||||
|
||||
request = request.defaults({jar: true});
|
||||
|
||||
describe('Post API', function () {
|
||||
|
||||
var user = testUtils.DataGenerator.forModel.users[0],
|
||||
csrfToken = '';
|
||||
|
||||
before(function (done) {
|
||||
testUtils.clearData()
|
||||
.then(function () {
|
||||
return testUtils.initData();
|
||||
})
|
||||
.then(function () {
|
||||
return testUtils.insertDefaultFixtures();
|
||||
})
|
||||
.then(function () {
|
||||
request.get(testUtils.API.getSigninURL(), function (error, response, body) {
|
||||
response.should.have.status(200);
|
||||
var pattern_meta = /<meta.*?name="csrf-param".*?content="(.*?)".*?>/i;
|
||||
pattern_meta.should.exist;
|
||||
csrfToken = body.match(pattern_meta)[1];
|
||||
setTimeout((function () {
|
||||
request.post({uri: testUtils.API.getSigninURL(),
|
||||
headers: {'X-CSRF-Token': csrfToken}}, function (error, response, body) {
|
||||
response.should.have.status(200);
|
||||
request.get(testUtils.API.getAdminURL(), function (error, response, body) {
|
||||
response.should.have.status(200);
|
||||
csrfToken = body.match(pattern_meta)[1];
|
||||
done();
|
||||
});
|
||||
}).form({email: user.email, password: user.password});
|
||||
}), 2000);
|
||||
});
|
||||
}, done);
|
||||
});
|
||||
|
||||
// ## Browse
|
||||
describe('Browse', function () {
|
||||
|
||||
it('retrieves all published posts only by default', function (done) {
|
||||
request.get(testUtils.API.getApiURL('posts/'), function (error, response, body) {
|
||||
response.should.have.status(200);
|
||||
should.not.exist(response.headers['x-cache-invalidate']);
|
||||
response.should.be.json;
|
||||
var jsonResponse = JSON.parse(body);
|
||||
jsonResponse.posts.should.exist;
|
||||
testUtils.API.checkResponse(jsonResponse, 'posts');
|
||||
jsonResponse.posts.should.have.length(5);
|
||||
testUtils.API.checkResponse(jsonResponse.posts[0], 'post');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('can retrieve all published posts and pages', function (done) {
|
||||
request.get(testUtils.API.getApiURL('posts/?staticPages=all'), function (error, response, body) {
|
||||
response.should.have.status(200);
|
||||
should.not.exist(response.headers['x-cache-invalidate']);
|
||||
response.should.be.json;
|
||||
var jsonResponse = JSON.parse(body);
|
||||
jsonResponse.posts.should.exist;
|
||||
testUtils.API.checkResponse(jsonResponse, 'posts');
|
||||
jsonResponse.posts.should.have.length(6);
|
||||
testUtils.API.checkResponse(jsonResponse.posts[0], 'post');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
// Test bits of the API we don't use in the app yet to ensure the API behaves properly
|
||||
|
||||
it('can retrieve all status posts and pages', function (done) {
|
||||
request.get(testUtils.API.getApiURL('posts/?staticPages=all&status=all'), function (error, response, body) {
|
||||
response.should.have.status(200);
|
||||
should.not.exist(response.headers['x-cache-invalidate']);
|
||||
response.should.be.json;
|
||||
var jsonResponse = JSON.parse(body);
|
||||
jsonResponse.posts.should.exist;
|
||||
testUtils.API.checkResponse(jsonResponse, 'posts');
|
||||
jsonResponse.posts.should.have.length(8);
|
||||
testUtils.API.checkResponse(jsonResponse.posts[0], 'post');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('can retrieve just published pages', function (done) {
|
||||
request.get(testUtils.API.getApiURL('posts/?staticPages=true'), function (error, response, body) {
|
||||
response.should.have.status(200);
|
||||
should.not.exist(response.headers['x-cache-invalidate']);
|
||||
response.should.be.json;
|
||||
var jsonResponse = JSON.parse(body);
|
||||
jsonResponse.posts.should.exist;
|
||||
testUtils.API.checkResponse(jsonResponse, 'posts');
|
||||
jsonResponse.posts.should.have.length(1);
|
||||
testUtils.API.checkResponse(jsonResponse.posts[0], 'post');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('can retrieve just draft posts', function (done) {
|
||||
request.get(testUtils.API.getApiURL('posts/?status=draft'), function (error, response, body) {
|
||||
response.should.have.status(200);
|
||||
should.not.exist(response.headers['x-cache-invalidate']);
|
||||
response.should.be.json;
|
||||
var jsonResponse = JSON.parse(body);
|
||||
jsonResponse.posts.should.exist;
|
||||
testUtils.API.checkResponse(jsonResponse, 'posts');
|
||||
jsonResponse.posts.should.have.length(1);
|
||||
testUtils.API.checkResponse(jsonResponse.posts[0], 'post');
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// ## Read
|
||||
describe('Read', function () {
|
||||
it('can retrieve a post', function (done) {
|
||||
request.get(testUtils.API.getApiURL('posts/1/'), function (error, response, body) {
|
||||
response.should.have.status(200);
|
||||
should.not.exist(response.headers['x-cache-invalidate']);
|
||||
response.should.be.json;
|
||||
var jsonResponse = JSON.parse(body);
|
||||
jsonResponse.should.exist;
|
||||
testUtils.API.checkResponse(jsonResponse, 'post');
|
||||
jsonResponse.page.should.eql(0);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('can retrieve a static page', function (done) {
|
||||
request.get(testUtils.API.getApiURL('posts/7/'), function (error, response, body) {
|
||||
response.should.have.status(200);
|
||||
should.not.exist(response.headers['x-cache-invalidate']);
|
||||
response.should.be.json;
|
||||
var jsonResponse = JSON.parse(body);
|
||||
jsonResponse.should.exist;
|
||||
testUtils.API.checkResponse(jsonResponse, 'post');
|
||||
jsonResponse.page.should.eql(1);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('can\'t retrieve non existent post', function (done) {
|
||||
request.get(testUtils.API.getApiURL('posts/99/'), function (error, response, body) {
|
||||
response.should.have.status(404);
|
||||
should.not.exist(response.headers['x-cache-invalidate']);
|
||||
response.should.be.json;
|
||||
var jsonResponse = JSON.parse(body);
|
||||
jsonResponse.should.exist;
|
||||
testUtils.API.checkResponseValue(jsonResponse, ['error']);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('can\'t retrieve a draft post', function (done) {
|
||||
request.get(testUtils.API.getApiURL('posts/5/'), function (error, response, body) {
|
||||
response.should.have.status(404);
|
||||
should.not.exist(response.headers['x-cache-invalidate']);
|
||||
response.should.be.json;
|
||||
var jsonResponse = JSON.parse(body);
|
||||
jsonResponse.should.exist;
|
||||
testUtils.API.checkResponseValue(jsonResponse, ['error']);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('can\'t retrieve a draft page', function (done) {
|
||||
request.get(testUtils.API.getApiURL('posts/8/'), function (error, response, body) {
|
||||
response.should.have.status(404);
|
||||
should.not.exist(response.headers['x-cache-invalidate']);
|
||||
response.should.be.json;
|
||||
var jsonResponse = JSON.parse(body);
|
||||
jsonResponse.should.exist;
|
||||
testUtils.API.checkResponseValue(jsonResponse, ['error']);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
// ## Add
|
||||
describe('Add', function () {
|
||||
it('can create a new draft, publish post, update post', function (done) {
|
||||
var newTitle = 'My Post',
|
||||
changedTitle = 'My Post changed',
|
||||
publishedState = 'published',
|
||||
newPost = {status: 'draft', title: newTitle, markdown: 'my post'};
|
||||
|
||||
request.post({uri: testUtils.API.getApiURL('posts/'),
|
||||
headers: {'X-CSRF-Token': csrfToken},
|
||||
json: newPost}, function (error, response, draftPost) {
|
||||
response.should.have.status(200);
|
||||
response.should.be.json;
|
||||
draftPost.should.exist;
|
||||
draftPost.title.should.eql(newTitle);
|
||||
draftPost.status = publishedState;
|
||||
testUtils.API.checkResponse(draftPost, 'post');
|
||||
request.put({uri: testUtils.API.getApiURL('posts/' + draftPost.id + '/'),
|
||||
headers: {'X-CSRF-Token': csrfToken},
|
||||
json: draftPost}, function (error, response, publishedPost) {
|
||||
response.should.have.status(200);
|
||||
response.headers['x-cache-invalidate'].should.eql('/, /page/*, /rss/, /rss/*, /' + publishedPost.slug + '/');
|
||||
response.should.be.json;
|
||||
publishedPost.should.exist;
|
||||
publishedPost.title.should.eql(newTitle);
|
||||
publishedPost.status.should.eql(publishedState);
|
||||
testUtils.API.checkResponse(publishedPost, 'post');
|
||||
request.put({uri: testUtils.API.getApiURL('posts/' + publishedPost.id + '/'),
|
||||
headers: {'X-CSRF-Token': csrfToken},
|
||||
json: publishedPost}, function (error, response, updatedPost) {
|
||||
response.should.have.status(200);
|
||||
response.headers['x-cache-invalidate'].should.eql('/, /page/*, /rss/, /rss/*, /' + updatedPost.slug + '/');
|
||||
response.should.be.json;
|
||||
updatedPost.should.exist;
|
||||
updatedPost.title.should.eql(newTitle);
|
||||
testUtils.API.checkResponse(updatedPost, 'post');
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// ## edit
|
||||
describe('Edit', function () {
|
||||
it('can edit a post', function (done) {
|
||||
request.get(testUtils.API.getApiURL('posts/1/'), function (error, response, body) {
|
||||
var jsonResponse = JSON.parse(body),
|
||||
changedValue = 'My new Title';
|
||||
jsonResponse.should.exist;
|
||||
jsonResponse.title = changedValue;
|
||||
|
||||
request.put({uri: testUtils.API.getApiURL('posts/1/'),
|
||||
headers: {'X-CSRF-Token': csrfToken},
|
||||
json: jsonResponse}, function (error, response, putBody) {
|
||||
response.should.have.status(200);
|
||||
response.headers['x-cache-invalidate'].should.eql('/, /page/*, /rss/, /rss/*, /' + putBody.slug + '/');
|
||||
response.should.be.json;
|
||||
putBody.should.exist;
|
||||
putBody.title.should.eql(changedValue);
|
||||
|
||||
testUtils.API.checkResponse(putBody, 'post');
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('can change a post to a static page', function (done) {
|
||||
request.get(testUtils.API.getApiURL('posts/1/'), function (error, response, body) {
|
||||
var jsonResponse = JSON.parse(body),
|
||||
changedValue = true;
|
||||
jsonResponse.should.exist;
|
||||
jsonResponse.page.should.eql(0);
|
||||
jsonResponse.page = changedValue;
|
||||
|
||||
request.put({uri: testUtils.API.getApiURL('posts/1/'),
|
||||
headers: {'X-CSRF-Token': csrfToken},
|
||||
json: jsonResponse}, function (error, response, putBody) {
|
||||
response.should.have.status(200);
|
||||
response.headers['x-cache-invalidate'].should.eql('/, /page/*, /rss/, /rss/*, /' + putBody.slug + '/');
|
||||
response.should.be.json;
|
||||
putBody.should.exist;
|
||||
putBody.page.should.eql(changedValue);
|
||||
|
||||
testUtils.API.checkResponse(putBody, 'post');
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('can change a static page to a post', function (done) {
|
||||
request.get(testUtils.API.getApiURL('posts/7/'), function (error, response, body) {
|
||||
var jsonResponse = JSON.parse(body),
|
||||
changedValue = false;
|
||||
jsonResponse.should.exist;
|
||||
jsonResponse.page.should.eql(1);
|
||||
jsonResponse.page = changedValue;
|
||||
|
||||
request.put({uri: testUtils.API.getApiURL('posts/1/'),
|
||||
headers: {'X-CSRF-Token': csrfToken},
|
||||
json: jsonResponse}, function (error, response, putBody) {
|
||||
response.should.have.status(200);
|
||||
response.headers['x-cache-invalidate'].should.eql('/, /page/*, /rss/, /rss/*, /' + putBody.slug + '/');
|
||||
response.should.be.json;
|
||||
putBody.should.exist;
|
||||
putBody.page.should.eql(changedValue);
|
||||
|
||||
testUtils.API.checkResponse(putBody, 'post');
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('can\'t edit a post with invalid CSRF token', function (done) {
|
||||
request.get(testUtils.API.getApiURL('posts/1/'), function (error, response, body) {
|
||||
var jsonResponse = JSON.parse(body);
|
||||
request.put({uri: testUtils.API.getApiURL('posts/1/'),
|
||||
headers: {'X-CSRF-Token': 'invalid-token'},
|
||||
json: jsonResponse}, function (error, response, putBody) {
|
||||
response.should.have.status(403);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('published_at = null', function (done) {
|
||||
request.get(testUtils.API.getApiURL('posts/1/'), function (error, response, body) {
|
||||
var jsonResponse = JSON.parse(body),
|
||||
changedValue = 'My new Title';
|
||||
jsonResponse.should.exist;
|
||||
jsonResponse.title = changedValue;
|
||||
jsonResponse.published_at = null;
|
||||
request.put({uri: testUtils.API.getApiURL('posts/1/'),
|
||||
headers: {'X-CSRF-Token': csrfToken},
|
||||
json: jsonResponse}, function (error, response, putBody) {
|
||||
response.should.have.status(200);
|
||||
response.headers['x-cache-invalidate'].should.eql('/, /page/*, /rss/, /rss/*, /' + putBody.slug + '/');
|
||||
response.should.be.json;
|
||||
putBody.should.exist;
|
||||
putBody.title.should.eql(changedValue);
|
||||
if (_.isEmpty(putBody.published_at)) {
|
||||
should.fail('null', 'valid date', 'publish_at should not be empty');
|
||||
done();
|
||||
}
|
||||
testUtils.API.checkResponse(putBody, 'post');
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// ## delete
|
||||
describe('Delete', function () {
|
||||
it('can\'t edit non existent post', function (done) {
|
||||
request.get(testUtils.API.getApiURL('posts/1/'), function (error, response, body) {
|
||||
var jsonResponse = JSON.parse(body),
|
||||
changedValue = 'My new Title';
|
||||
jsonResponse.title.exist;
|
||||
jsonResponse.testvalue = changedValue;
|
||||
jsonResponse.id = 99;
|
||||
request.put({uri: testUtils.API.getApiURL('posts/99/'),
|
||||
headers: {'X-CSRF-Token': csrfToken},
|
||||
json: jsonResponse}, function (error, response, putBody) {
|
||||
response.should.have.status(404);
|
||||
should.not.exist(response.headers['x-cache-invalidate']);
|
||||
response.should.be.json;
|
||||
testUtils.API.checkResponseValue(putBody, ['error']);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('can delete a post', function (done) {
|
||||
var deletePostId = 1;
|
||||
request.del({uri: testUtils.API.getApiURL('posts/' + deletePostId + '/'),
|
||||
headers: {'X-CSRF-Token': csrfToken}}, function (error, response, body) {
|
||||
response.should.have.status(200);
|
||||
response.should.be.json;
|
||||
var jsonResponse = JSON.parse(body);
|
||||
jsonResponse.should.exist;
|
||||
response.headers['x-cache-invalidate'].should.eql('/, /page/*, /rss/, /rss/*, /' + jsonResponse.slug + '/');
|
||||
testUtils.API.checkResponse(jsonResponse, 'post');
|
||||
jsonResponse.id.should.eql(deletePostId);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('can\'t delete a non existent post', function (done) {
|
||||
request.del({uri: testUtils.API.getApiURL('posts/99/'),
|
||||
headers: {'X-CSRF-Token': csrfToken}}, function (error, response, body) {
|
||||
response.should.have.status(404);
|
||||
should.not.exist(response.headers['x-cache-invalidate']);
|
||||
response.should.be.json;
|
||||
var jsonResponse = JSON.parse(body);
|
||||
jsonResponse.should.exist;
|
||||
testUtils.API.checkResponseValue(jsonResponse, ['error']);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('can delete a new draft', function (done) {
|
||||
var newTitle = 'My Post',
|
||||
publishedState = 'draft',
|
||||
newPost = {status: publishedState, title: newTitle, markdown: 'my post'};
|
||||
|
||||
request.post({uri: testUtils.API.getApiURL('posts/'),
|
||||
headers: {'X-CSRF-Token': csrfToken},
|
||||
json: newPost}, function (error, response, draftPost) {
|
||||
response.should.have.status(200);
|
||||
|
||||
response.should.be.json;
|
||||
draftPost.should.exist;
|
||||
draftPost.title.should.eql(newTitle);
|
||||
draftPost.status = publishedState;
|
||||
testUtils.API.checkResponse(draftPost, 'post');
|
||||
request.del({uri: testUtils.API.getApiURL('posts/' + draftPost.id + '/'),
|
||||
headers: {'X-CSRF-Token': csrfToken}}, function (error, response, body) {
|
||||
response.should.have.status(200);
|
||||
response.should.be.json;
|
||||
var jsonResponse = JSON.parse(body);
|
||||
jsonResponse.should.exist;
|
||||
testUtils.API.checkResponse(jsonResponse, 'post');
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Dated Permalinks', function () {
|
||||
before(function (done) {
|
||||
request.get(testUtils.API.getApiURL('settings'), function (error, response, body) {
|
||||
if (error) { done(error); }
|
||||
var jsonResponse = JSON.parse(body);
|
||||
jsonResponse.permalinks = '/:year/:month/:day/:slug/';
|
||||
|
||||
request.put({
|
||||
uri: testUtils.API.getApiURL('settings/'),
|
||||
headers: {'X-CSRF-Token': csrfToken},
|
||||
json: jsonResponse
|
||||
}, function (error, response, putBody) {
|
||||
if (error) { done(error); }
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
after(function (done) {
|
||||
request.get(testUtils.API.getApiURL('settings'), function (error, response, body) {
|
||||
if (error) { done(error); }
|
||||
var jsonResponse = JSON.parse(body);
|
||||
jsonResponse.permalinks = '/:slug/';
|
||||
|
||||
request.put({
|
||||
uri: testUtils.API.getApiURL('settings/'),
|
||||
headers: {'X-CSRF-Token': csrfToken},
|
||||
json: jsonResponse
|
||||
}, function (error, response, putBody) {
|
||||
if (error) { done(error); }
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('Can read a post', function (done) {
|
||||
// nothing should have changed here
|
||||
request.get(testUtils.API.getApiURL('posts/2/'), function (error, response, body) {
|
||||
if (error) { done(error); }
|
||||
response.should.have.status(200);
|
||||
should.not.exist(response.headers['x-cache-invalidate']);
|
||||
response.should.be.json;
|
||||
|
||||
var jsonResponse = JSON.parse(body);
|
||||
jsonResponse.should.exist;
|
||||
testUtils.API.checkResponse(jsonResponse, 'post');
|
||||
jsonResponse.slug.should.not.match(/^\/[0-9]{4}\/[0-9]{2}\/[0-9]{2}/);
|
||||
jsonResponse.page.should.eql(0);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('Can edit a post', function (done) {
|
||||
request.get(testUtils.API.getApiURL('posts/2/'), function (error, response, body) {
|
||||
if (error) { done(error); }
|
||||
var jsonResponse = JSON.parse(body),
|
||||
changedValue = 'My new Title';
|
||||
jsonResponse.should.exist;
|
||||
jsonResponse.title = changedValue;
|
||||
|
||||
request.put({
|
||||
uri: testUtils.API.getApiURL('posts/2/'),
|
||||
headers: {'X-CSRF-Token': csrfToken},
|
||||
json: jsonResponse
|
||||
}, function (error, response, putBody) {
|
||||
if (error) { done(error); }
|
||||
var today = new Date(),
|
||||
dd = ("0" + today.getDate()).slice(-2),
|
||||
mm = ("0" + (today.getMonth() + 1)).slice(-2),
|
||||
yyyy = today.getFullYear(),
|
||||
postLink = '/' + yyyy + '/' + mm + '/' + dd + '/' + putBody.slug + '/';
|
||||
|
||||
response.should.have.status(200);
|
||||
response.headers['x-cache-invalidate'].should.eql('/, /page/*, /rss/, /rss/*, ' + postLink);
|
||||
response.should.be.json;
|
||||
putBody.should.exist;
|
||||
putBody.title.should.eql(changedValue);
|
||||
|
||||
testUtils.API.checkResponse(putBody, 'post');
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@ -1,137 +0,0 @@
|
||||
/*globals describe, before, beforeEach, afterEach, it */
|
||||
var testUtils = require('../../utils'),
|
||||
should = require('should'),
|
||||
_ = require('lodash'),
|
||||
request = require('request');
|
||||
|
||||
request = request.defaults({jar:true})
|
||||
|
||||
describe('Settings API', function () {
|
||||
|
||||
var user = testUtils.DataGenerator.forModel.users[0],
|
||||
csrfToken = '';
|
||||
|
||||
before(function (done) {
|
||||
testUtils.clearData()
|
||||
.then(function () {
|
||||
return testUtils.initData();
|
||||
})
|
||||
.then(function () {
|
||||
return testUtils.insertDefaultFixtures();
|
||||
})
|
||||
.then(function () {
|
||||
request.get(testUtils.API.getSigninURL(), function (error, response, body) {
|
||||
response.should.have.status(200);
|
||||
var pattern_meta = /<meta.*?name="csrf-param".*?content="(.*?)".*?>/i;
|
||||
pattern_meta.should.exist;
|
||||
csrfToken = body.match(pattern_meta)[1];
|
||||
setTimeout((function () {
|
||||
request.post({uri: testUtils.API.getSigninURL(),
|
||||
headers: {'X-CSRF-Token': csrfToken}}, function (error, response, body) {
|
||||
response.should.have.status(200);
|
||||
request.get(testUtils.API.getAdminURL(), function (error, response, body) {
|
||||
response.should.have.status(200);
|
||||
csrfToken = body.match(pattern_meta)[1];
|
||||
done();
|
||||
});
|
||||
}).form({email: user.email, password: user.password});
|
||||
}), 2000);
|
||||
});
|
||||
}, done);
|
||||
});
|
||||
|
||||
// TODO: currently includes values of type=core
|
||||
it('can retrieve all settings', function (done) {
|
||||
request.get(testUtils.API.getApiURL('settings/'), function (error, response, body) {
|
||||
response.should.have.status(200);
|
||||
should.not.exist(response.headers['x-cache-invalidate']);
|
||||
response.should.be.json;
|
||||
var jsonResponse = JSON.parse(body);
|
||||
jsonResponse.should.exist;
|
||||
|
||||
testUtils.API.checkResponse(jsonResponse, 'settings');
|
||||
done();
|
||||
});
|
||||
});
|
||||
it('can retrieve a setting', function (done) {
|
||||
request.get(testUtils.API.getApiURL('settings/title/'), function (error, response, body) {
|
||||
response.should.have.status(200);
|
||||
should.not.exist(response.headers['x-cache-invalidate']);
|
||||
response.should.be.json;
|
||||
var jsonResponse = JSON.parse(body);
|
||||
|
||||
jsonResponse.should.exist;
|
||||
testUtils.API.checkResponseValue(jsonResponse, ['key','value']);
|
||||
jsonResponse.key.should.eql('title');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('can\'t retrieve non existent setting', function (done) {
|
||||
request.get(testUtils.API.getApiURL('settings/testsetting/'), function (error, response, body) {
|
||||
response.should.have.status(404);
|
||||
should.not.exist(response.headers['x-cache-invalidate']);
|
||||
response.should.be.json;
|
||||
var jsonResponse = JSON.parse(body);
|
||||
jsonResponse.should.exist;
|
||||
testUtils.API.checkResponseValue(jsonResponse, ['error']);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('can edit settings', function (done) {
|
||||
request.get(testUtils.API.getApiURL('settings'), function (error, response, body) {
|
||||
var jsonResponse = JSON.parse(body),
|
||||
changedValue = 'Ghost changed';
|
||||
jsonResponse.should.exist;
|
||||
jsonResponse.title = changedValue;
|
||||
|
||||
request.put({uri: testUtils.API.getApiURL('settings/'),
|
||||
headers: {'X-CSRF-Token': csrfToken},
|
||||
json: jsonResponse}, function (error, response, putBody) {
|
||||
response.should.have.status(200);
|
||||
response.headers['x-cache-invalidate'].should.eql('/*');
|
||||
response.should.be.json;
|
||||
putBody.should.exist;
|
||||
putBody.title.should.eql(changedValue);
|
||||
testUtils.API.checkResponse(putBody, 'settings');
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('can\'t edit settings with invalid CSRF token', function (done) {
|
||||
request.get(testUtils.API.getApiURL('settings'), function (error, response, body) {
|
||||
var jsonResponse = JSON.parse(body),
|
||||
changedValue = 'Ghost changed';
|
||||
jsonResponse.should.exist;
|
||||
jsonResponse.title = changedValue;
|
||||
|
||||
request.put({uri: testUtils.API.getApiURL('settings/'),
|
||||
headers: {'X-CSRF-Token': 'invalid-token'},
|
||||
json: jsonResponse}, function (error, response, putBody) {
|
||||
response.should.have.status(403);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('can\'t edit non existent setting', function (done) {
|
||||
request.get(testUtils.API.getApiURL('settings'), function (error, response, body) {
|
||||
var jsonResponse = JSON.parse(body),
|
||||
newValue = 'new value';
|
||||
jsonResponse.should.exist;
|
||||
jsonResponse.testvalue = newValue;
|
||||
|
||||
request.put({uri: testUtils.API.getApiURL('settings/'),
|
||||
headers: {'X-CSRF-Token': csrfToken},
|
||||
json: jsonResponse}, function (error, response, putBody) {
|
||||
response.should.have.status(404);
|
||||
should.not.exist(response.headers['x-cache-invalidate']);
|
||||
response.should.be.json;
|
||||
testUtils.API.checkResponseValue(putBody, ['error']);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@ -1,55 +0,0 @@
|
||||
/*globals describe, before, beforeEach, afterEach, it */
|
||||
var testUtils = require('../../utils'),
|
||||
should = require('should'),
|
||||
_ = require('lodash'),
|
||||
request = require('request');
|
||||
|
||||
request = request.defaults({jar:true})
|
||||
|
||||
describe('Tag API', function () {
|
||||
|
||||
var user = testUtils.DataGenerator.forModel.users[0],
|
||||
csrfToken = '';
|
||||
|
||||
before(function (done) {
|
||||
testUtils.clearData()
|
||||
.then(function () {
|
||||
return testUtils.initData();
|
||||
})
|
||||
.then(function () {
|
||||
return testUtils.insertDefaultFixtures();
|
||||
})
|
||||
.then(function () {
|
||||
request.get(testUtils.API.getSigninURL(), function (error, response, body) {
|
||||
response.should.have.status(200);
|
||||
var pattern_meta = /<meta.*?name="csrf-param".*?content="(.*?)".*?>/i;
|
||||
pattern_meta.should.exist;
|
||||
csrfToken = body.match(pattern_meta)[1];
|
||||
setTimeout((function () {
|
||||
request.post({uri: testUtils.API.getSigninURL(),
|
||||
headers: {'X-CSRF-Token': csrfToken}}, function (error, response, body) {
|
||||
response.should.have.status(200);
|
||||
request.get(testUtils.API.getAdminURL(), function (error, response, body) {
|
||||
response.should.have.status(200);
|
||||
csrfToken = body.match(pattern_meta)[1];
|
||||
done();
|
||||
});
|
||||
}).form({email: user.email, password: user.password});
|
||||
}), 2000);
|
||||
});
|
||||
}, done);
|
||||
});
|
||||
|
||||
it('can retrieve all tags', function (done) {
|
||||
request.get(testUtils.API.getApiURL('tags/'), function (error, response, body) {
|
||||
response.should.have.status(200);
|
||||
should.not.exist(response.headers['x-cache-invalidate']);
|
||||
response.should.be.json;
|
||||
var jsonResponse = JSON.parse(body);
|
||||
jsonResponse.should.exist;
|
||||
jsonResponse.should.have.length(6);
|
||||
testUtils.API.checkResponse(jsonResponse[0], 'tag');
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
@ -1,119 +0,0 @@
|
||||
/*globals describe, before, beforeEach, afterEach, it */
|
||||
var testUtils = require('../../utils'),
|
||||
should = require('should'),
|
||||
_ = require('lodash'),
|
||||
request = require('request');
|
||||
|
||||
request = request.defaults({jar:true})
|
||||
|
||||
describe('User API', function () {
|
||||
|
||||
var user = testUtils.DataGenerator.forModel.users[0],
|
||||
csrfToken = '';
|
||||
|
||||
before(function (done) {
|
||||
testUtils.clearData()
|
||||
.then(function () {
|
||||
return testUtils.initData();
|
||||
})
|
||||
.then(function () {
|
||||
return testUtils.insertDefaultFixtures();
|
||||
})
|
||||
.then(function () {
|
||||
request.get(testUtils.API.getSigninURL(), function (error, response, body) {
|
||||
response.should.have.status(200);
|
||||
var pattern_meta = /<meta.*?name="csrf-param".*?content="(.*?)".*?>/i;
|
||||
pattern_meta.should.exist;
|
||||
csrfToken = body.match(pattern_meta)[1];
|
||||
setTimeout((function () {
|
||||
request.post({uri: testUtils.API.getSigninURL(),
|
||||
headers: {'X-CSRF-Token': csrfToken}}, function (error, response, body) {
|
||||
response.should.have.status(200);
|
||||
request.get(testUtils.API.getAdminURL(), function (error, response, body) {
|
||||
response.should.have.status(200);
|
||||
csrfToken = body.match(pattern_meta)[1];
|
||||
done();
|
||||
});
|
||||
}).form({email: user.email, password: user.password});
|
||||
}), 2000);
|
||||
});
|
||||
}, done);
|
||||
});
|
||||
|
||||
it('can retrieve all users', function (done) {
|
||||
request.get(testUtils.API.getApiURL('users/'), function (error, response, body) {
|
||||
response.should.have.status(200);
|
||||
should.not.exist(response.headers['x-cache-invalidate']);
|
||||
response.should.be.json;
|
||||
var jsonResponse = JSON.parse(body);
|
||||
jsonResponse[0].should.exist;
|
||||
|
||||
testUtils.API.checkResponse(jsonResponse[0], 'user');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('can retrieve a user', function (done) {
|
||||
request.get(testUtils.API.getApiURL('users/me/'), function (error, response, body) {
|
||||
response.should.have.status(200);
|
||||
should.not.exist(response.headers['x-cache-invalidate']);
|
||||
response.should.be.json;
|
||||
var jsonResponse = JSON.parse(body);
|
||||
jsonResponse.should.exist;
|
||||
|
||||
testUtils.API.checkResponse(jsonResponse, 'user');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('can\'t retrieve non existent user', function (done) {
|
||||
request.get(testUtils.API.getApiURL('users/99/'), function (error, response, body) {
|
||||
response.should.have.status(404);
|
||||
should.not.exist(response.headers['x-cache-invalidate']);
|
||||
response.should.be.json;
|
||||
var jsonResponse = JSON.parse(body);
|
||||
jsonResponse.should.exist;
|
||||
|
||||
testUtils.API.checkResponseValue(jsonResponse, ['error']);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('can edit a user', function (done) {
|
||||
request.get(testUtils.API.getApiURL('users/me/'), function (error, response, body) {
|
||||
var jsonResponse = JSON.parse(body),
|
||||
changedValue = 'joe-bloggs.ghost.org';
|
||||
jsonResponse.should.exist;
|
||||
jsonResponse.website = changedValue;
|
||||
|
||||
request.put({uri: testUtils.API.getApiURL('users/me/'),
|
||||
headers: {'X-CSRF-Token': csrfToken},
|
||||
json: jsonResponse}, function (error, response, putBody) {
|
||||
response.should.have.status(200);
|
||||
response.headers['x-cache-invalidate'].should.eql('/*');
|
||||
response.should.be.json;
|
||||
putBody.should.exist;
|
||||
putBody.website.should.eql(changedValue);
|
||||
|
||||
testUtils.API.checkResponse(putBody, 'user');
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('can\'t edit a user with invalid CSRF token', function (done) {
|
||||
request.get(testUtils.API.getApiURL('users/me/'), function (error, response, body) {
|
||||
var jsonResponse = JSON.parse(body),
|
||||
changedValue = 'joe-bloggs.ghost.org';
|
||||
jsonResponse.should.exist;
|
||||
jsonResponse.website = changedValue;
|
||||
|
||||
request.put({uri: testUtils.API.getApiURL('users/me/'),
|
||||
headers: {'X-CSRF-Token': 'invalid-token'},
|
||||
json: jsonResponse}, function (error, response, putBody) {
|
||||
response.should.have.status(403);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@ -49,12 +49,16 @@ var DEBUG = false, // TOGGLE THIS TO GET MORE SCREENSHOTS
|
||||
casper.writeContentToCodeMirror = function (content) {
|
||||
var lines = content.split("\n");
|
||||
|
||||
casper.each(lines, function (self, line) {
|
||||
self.sendKeys('.CodeMirror-wrap textarea', line, {keepFocus: true});
|
||||
self.sendKeys('.CodeMirror-wrap textarea', casper.page.event.key.Enter, {keepFocus: true});
|
||||
});
|
||||
casper.waitForSelector('.CodeMirror-wrap textarea', function onSuccess() {
|
||||
casper.each(lines, function (self, line) {
|
||||
self.sendKeys('.CodeMirror-wrap textarea', line, {keepFocus: true});
|
||||
self.sendKeys('.CodeMirror-wrap textarea', casper.page.event.key.Enter, {keepFocus: true});
|
||||
});
|
||||
|
||||
return this;
|
||||
return this;
|
||||
}, function onTimeout() {
|
||||
casper.test.fail('CodeMirror was not found.');
|
||||
}, 2000);
|
||||
};
|
||||
|
||||
casper.waitForOpaque = function (classname, then, timeout) {
|
||||
|
@ -10,7 +10,7 @@ CasperTest.begin('Ensure that RSS is available', 11, function suite(test) {
|
||||
siteDescription = '<description><![CDATA[Just a blogging platform.]]></description>',
|
||||
siteUrl = '<link>http://127.0.0.1:2369/</link>',
|
||||
postTitle = '<![CDATA[Welcome to Ghost]]>',
|
||||
postStart = '<description><![CDATA[<p>You’re live!',
|
||||
postStart = '<description><![CDATA[<p>You\'re live!',
|
||||
postEnd = 'you think :)</p>]]></description>',
|
||||
postLink = '<link>http://127.0.0.1:2369/welcome-to-ghost/</link>',
|
||||
postCreator = '<dc:creator><![CDATA[Test User]]>';
|
||||
@ -29,17 +29,17 @@ CasperTest.begin('Ensure that RSS is available', 11, function suite(test) {
|
||||
});
|
||||
}, false);
|
||||
|
||||
CasperTest.begin('Ensure that author element is not included. Only dc:creator', 3, function suite(test) {
|
||||
CasperTest.begin('Ensure that author element is not included. Only dc:creator', 3, function suite(test) {
|
||||
CasperTest.Routines.togglePermalinks.run('off');
|
||||
casper.thenOpen(url + 'rss/', function (response) {
|
||||
casper.thenOpen(url + 'rss/', function (response) {
|
||||
var content = this.getPageContent(),
|
||||
author = '<author>',
|
||||
postCreator = '<dc:creator><![CDATA[Test User]]>';
|
||||
|
||||
test.assertEqual(response.status, 200, 'Response status should be 200.');
|
||||
test.assert(content.indexOf(author) < 0, 'Author element should not be included');
|
||||
test.assert(content.indexOf(postCreator) >= 0, 'Welcome post should have Test User as the creator.');
|
||||
});
|
||||
test.assert(content.indexOf(postCreator) >= 0, 'Welcome post should have Test User as the creator.');
|
||||
});
|
||||
}, false);
|
||||
|
||||
CasperTest.begin('Ensures dated permalinks works with RSS', 2, function suite(test) {
|
||||
|
@ -6,11 +6,13 @@
|
||||
// But then again testing real code, rather than mock code, might be more useful...
|
||||
|
||||
var request = require('supertest'),
|
||||
express = require('express'),
|
||||
should = require('should'),
|
||||
moment = require('moment'),
|
||||
|
||||
testUtils = require('../../utils'),
|
||||
config = require('../../../server/config'),
|
||||
ghost = require('../../../../core'),
|
||||
httpServer,
|
||||
|
||||
ONE_HOUR_S = 60 * 60,
|
||||
ONE_YEAR_S = 365 * 24 * ONE_HOUR_S,
|
||||
@ -37,16 +39,77 @@ describe('Admin Routing', function () {
|
||||
};
|
||||
}
|
||||
|
||||
before(function (done) {
|
||||
testUtils.clearData().then(function () {
|
||||
// we initialise data, but not a user.
|
||||
return testUtils.initData();
|
||||
}).then(function () {
|
||||
done();
|
||||
}, done);
|
||||
function doEndNoAuth(done) {
|
||||
return function (err, res) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
// Setup the request object with the correct URL
|
||||
request = request(config().url);
|
||||
should.not.exist(res.headers['x-cache-invalidate']);
|
||||
should.not.exist(res.headers['X-CSRF-Token']);
|
||||
should.not.exist(res.headers['set-cookie']);
|
||||
should.exist(res.headers.date);
|
||||
|
||||
done();
|
||||
};
|
||||
}
|
||||
|
||||
before(function (done) {
|
||||
var app = express();
|
||||
|
||||
ghost({app: app}).then(function (_httpServer) {
|
||||
// Setup the request object with the ghost express app
|
||||
httpServer = _httpServer;
|
||||
request = request(app);
|
||||
testUtils.clearData().then(function () {
|
||||
// we initialise data, but not a user. No user should be required for navigating the frontend
|
||||
return testUtils.initData();
|
||||
}).then(function () {
|
||||
done();
|
||||
}, done);
|
||||
}).otherwise(function (e) {
|
||||
console.log('Ghost Error: ', e);
|
||||
console.log(e.stack);
|
||||
});
|
||||
});
|
||||
|
||||
after(function () {
|
||||
httpServer.close();
|
||||
});
|
||||
|
||||
describe('Legacy Redirects', function () {
|
||||
|
||||
it('should redirect /logout/ to /ghost/signout/', function (done) {
|
||||
request.get('/logout/')
|
||||
.expect('Location', '/ghost/signout/')
|
||||
.expect('Cache-Control', cacheRules.year)
|
||||
.expect(301)
|
||||
.end(doEndNoAuth(done));
|
||||
});
|
||||
|
||||
it('should redirect /signout/ to /ghost/signout/', function (done) {
|
||||
request.get('/signout/')
|
||||
.expect('Location', '/ghost/signout/')
|
||||
.expect('Cache-Control', cacheRules.year)
|
||||
.expect(301)
|
||||
.end(doEndNoAuth(done));
|
||||
});
|
||||
|
||||
it('should redirect /signin/ to /ghost/signin/', function (done) {
|
||||
request.get('/signin/')
|
||||
.expect('Location', '/ghost/signin/')
|
||||
.expect('Cache-Control', cacheRules.year)
|
||||
.expect(301)
|
||||
.end(doEndNoAuth(done));
|
||||
});
|
||||
|
||||
it('should redirect /signup/ to /ghost/signup/', function (done) {
|
||||
request.get('/signup/')
|
||||
.expect('Location', '/ghost/signup/')
|
||||
.expect('Cache-Control', cacheRules.year)
|
||||
.expect(301)
|
||||
.end(doEndNoAuth(done));
|
||||
});
|
||||
});
|
||||
|
||||
describe('Ghost Admin Signup', function () {
|
||||
|
66
core/test/functional/routes/api/error_test.js
Normal file
66
core/test/functional/routes/api/error_test.js
Normal file
@ -0,0 +1,66 @@
|
||||
/*global describe, it, before, after */
|
||||
|
||||
// # Api Route tests
|
||||
// As it stands, these tests depend on the database, and as such are integration tests.
|
||||
// Mocking out the models to not touch the DB would turn these into unit tests, and should probably be done in future,
|
||||
// But then again testing real code, rather than mock code, might be more useful...
|
||||
|
||||
var supertest = require('supertest'),
|
||||
express = require('express'),
|
||||
should = require('should'),
|
||||
_ = require('lodash'),
|
||||
testUtils = require('../../../utils'),
|
||||
|
||||
ghost = require('../../../../../core'),
|
||||
|
||||
httpServer,
|
||||
request,
|
||||
agent;
|
||||
|
||||
describe('Unauthorized', function () {
|
||||
|
||||
before(function (done) {
|
||||
var app = express();
|
||||
|
||||
ghost({app: app}).then(function (_httpServer) {
|
||||
httpServer = _httpServer;
|
||||
// request = supertest(app);
|
||||
request = supertest.agent(app);
|
||||
testUtils.clearData().then(function () {
|
||||
// we initialise data, but not a user.
|
||||
return testUtils.initData();
|
||||
}).then(function () {
|
||||
done();
|
||||
}, done);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
after(function () {
|
||||
httpServer.close();
|
||||
});
|
||||
|
||||
|
||||
describe('Unauthorized', function () {
|
||||
it('can\'t retrieve posts', function (done) {
|
||||
request.get(testUtils.API.getApiQuery('posts/'))
|
||||
.expect(401)
|
||||
.end(function firstRequest(err, res) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
should.not.exist(res.headers['x-cache-invalidate']);
|
||||
res.should.be.json;
|
||||
var jsonResponse = res.body;
|
||||
jsonResponse.should.exist;
|
||||
testUtils.API.checkResponseValue(jsonResponse, ['error']);
|
||||
done();
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
});
|
729
core/test/functional/routes/api/posts_test.js
Normal file
729
core/test/functional/routes/api/posts_test.js
Normal file
@ -0,0 +1,729 @@
|
||||
/*global describe, it, before, after */
|
||||
var supertest = require('supertest'),
|
||||
express = require('express'),
|
||||
should = require('should'),
|
||||
_ = require('lodash'),
|
||||
testUtils = require('../../../utils'),
|
||||
|
||||
ghost = require('../../../../../core'),
|
||||
|
||||
httpServer,
|
||||
request,
|
||||
agent;
|
||||
|
||||
|
||||
describe('Post API', function () {
|
||||
var user = testUtils.DataGenerator.forModel.users[0],
|
||||
csrfToken = '';
|
||||
|
||||
before(function (done) {
|
||||
var app = express();
|
||||
|
||||
ghost({app: app}).then(function (_httpServer) {
|
||||
httpServer = _httpServer;
|
||||
|
||||
request = supertest.agent(app);
|
||||
|
||||
testUtils.clearData()
|
||||
.then(function () {
|
||||
return testUtils.initData();
|
||||
})
|
||||
.then(function () {
|
||||
return testUtils.insertDefaultFixtures();
|
||||
})
|
||||
.then(function () {
|
||||
|
||||
request.get('/ghost/signin/')
|
||||
.expect(200)
|
||||
.end(function (err, res) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
var pattern_meta = /<meta.*?name="csrf-param".*?content="(.*?)".*?>/i;
|
||||
pattern_meta.should.exist;
|
||||
csrfToken = res.text.match(pattern_meta)[1];
|
||||
|
||||
setTimeout(function () {
|
||||
request.post('/ghost/signin/')
|
||||
.set('X-CSRF-Token', csrfToken)
|
||||
.send({email: user.email, password: user.password})
|
||||
.expect(200)
|
||||
.end(function (err, res) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
|
||||
request.saveCookies(res);
|
||||
request.get('/ghost/')
|
||||
.expect(200)
|
||||
.end(function (err, res) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
csrfToken = res.text.match(pattern_meta)[1];
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
}, 2000);
|
||||
|
||||
});
|
||||
}, done);
|
||||
}).otherwise(function (e) {
|
||||
console.log('Ghost Error: ', e);
|
||||
console.log(e.stack);
|
||||
});
|
||||
});
|
||||
|
||||
after(function () {
|
||||
httpServer.close();
|
||||
});
|
||||
|
||||
|
||||
describe('Browse', function () {
|
||||
|
||||
it('retrieves all published posts only by default', function (done) {
|
||||
request.get(testUtils.API.getApiQuery('posts/'))
|
||||
.expect(200)
|
||||
.end(function (err, res) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
should.not.exist(res.headers['x-cache-invalidate']);
|
||||
res.should.be.json;
|
||||
var jsonResponse = res.body;
|
||||
jsonResponse.posts.should.exist;
|
||||
testUtils.API.checkResponse(jsonResponse, 'posts');
|
||||
jsonResponse.posts.should.have.length(5);
|
||||
testUtils.API.checkResponse(jsonResponse.posts[0], 'post');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('can retrieve all published posts and pages', function (done) {
|
||||
request.get(testUtils.API.getApiQuery('posts/?staticPages=all'))
|
||||
.expect(200)
|
||||
.end(function (err, res) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
should.not.exist(res.headers['x-cache-invalidate']);
|
||||
res.should.be.json;
|
||||
var jsonResponse = res.body;
|
||||
jsonResponse.posts.should.exist;
|
||||
testUtils.API.checkResponse(jsonResponse, 'posts');
|
||||
jsonResponse.posts.should.have.length(6);
|
||||
testUtils.API.checkResponse(jsonResponse.posts[0], 'post');
|
||||
done();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
// Test bits of the API we don't use in the app yet to ensure the API behaves properly
|
||||
|
||||
it('can retrieve all status posts and pages', function (done) {
|
||||
request.get(testUtils.API.getApiQuery('posts/?staticPages=all&status=all'))
|
||||
.expect(200)
|
||||
.end(function (err, res) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
should.not.exist(res.headers['x-cache-invalidate']);
|
||||
res.should.be.json;
|
||||
var jsonResponse = res.body;
|
||||
jsonResponse.posts.should.exist;
|
||||
testUtils.API.checkResponse(jsonResponse, 'posts');
|
||||
jsonResponse.posts.should.have.length(8);
|
||||
testUtils.API.checkResponse(jsonResponse.posts[0], 'post');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('can retrieve just published pages', function (done) {
|
||||
request.get(testUtils.API.getApiQuery('posts/?staticPages=true'))
|
||||
.expect(200)
|
||||
.end(function (err, res) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
should.not.exist(res.headers['x-cache-invalidate']);
|
||||
res.should.be.json;
|
||||
var jsonResponse = res.body;
|
||||
jsonResponse.posts.should.exist;
|
||||
testUtils.API.checkResponse(jsonResponse, 'posts');
|
||||
jsonResponse.posts.should.have.length(1);
|
||||
testUtils.API.checkResponse(jsonResponse.posts[0], 'post');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('can retrieve just draft posts', function (done) {
|
||||
request.get(testUtils.API.getApiQuery('posts/?status=draft'))
|
||||
.expect(200)
|
||||
.end(function (err, res) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
should.not.exist(res.headers['x-cache-invalidate']);
|
||||
res.should.be.json;
|
||||
var jsonResponse = res.body;
|
||||
jsonResponse.posts.should.exist;
|
||||
testUtils.API.checkResponse(jsonResponse, 'posts');
|
||||
jsonResponse.posts.should.have.length(1);
|
||||
testUtils.API.checkResponse(jsonResponse.posts[0], 'post');
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
// ## Read
|
||||
describe('Read', function () {
|
||||
it('can retrieve a post', function (done) {
|
||||
request.get(testUtils.API.getApiQuery('posts/1/'))
|
||||
.end(function (err, res) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
res.should.have.status(200);
|
||||
should.not.exist(res.headers['x-cache-invalidate']);
|
||||
res.should.be.json;
|
||||
var jsonResponse = res.body;
|
||||
jsonResponse.should.exist;
|
||||
testUtils.API.checkResponse(jsonResponse, 'post');
|
||||
jsonResponse.page.should.eql(0);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('can retrieve a static page', function (done) {
|
||||
request.get(testUtils.API.getApiQuery('posts/7/'))
|
||||
.expect(200)
|
||||
.end(function (err, res) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
should.not.exist(res.headers['x-cache-invalidate']);
|
||||
res.should.be.json;
|
||||
var jsonResponse = res.body;
|
||||
jsonResponse.should.exist;
|
||||
testUtils.API.checkResponse(jsonResponse, 'post');
|
||||
jsonResponse.page.should.eql(1);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('can\'t retrieve non existent post', function (done) {
|
||||
request.get(testUtils.API.getApiQuery('posts/99/'))
|
||||
.expect(404)
|
||||
.end(function (err, res) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
should.not.exist(res.headers['x-cache-invalidate']);
|
||||
res.should.be.json;
|
||||
var jsonResponse = res.body;
|
||||
jsonResponse.should.exist;
|
||||
testUtils.API.checkResponseValue(jsonResponse, ['error']);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('can\'t retrieve a draft post', function (done) {
|
||||
request.get(testUtils.API.getApiQuery('posts/5/'))
|
||||
.expect(404)
|
||||
.end(function (err, res) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
should.not.exist(res.headers['x-cache-invalidate']);
|
||||
res.should.be.json;
|
||||
var jsonResponse = res.body;
|
||||
jsonResponse.should.exist;
|
||||
testUtils.API.checkResponseValue(jsonResponse, ['error']);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('can\'t retrieve a draft page', function (done) {
|
||||
request.get(testUtils.API.getApiQuery('posts/8/'))
|
||||
.expect(404)
|
||||
.end(function (err, res) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
should.not.exist(res.headers['x-cache-invalidate']);
|
||||
res.should.be.json;
|
||||
var jsonResponse = res.body;
|
||||
jsonResponse.should.exist;
|
||||
testUtils.API.checkResponseValue(jsonResponse, ['error']);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
// ## Add
|
||||
describe('Add', function () {
|
||||
it('can create a new draft, publish post, update post', function (done) {
|
||||
var newTitle = 'My Post',
|
||||
changedTitle = 'My Post changed',
|
||||
publishedState = 'published',
|
||||
newPost = {status: 'draft', title: newTitle, markdown: 'my post'};
|
||||
|
||||
request.post(testUtils.API.getApiQuery('posts/'))
|
||||
.set('X-CSRF-Token', csrfToken)
|
||||
.send(newPost)
|
||||
.expect(200)
|
||||
.end(function (err, res) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
res.should.be.json;
|
||||
var draftPost = res.body;
|
||||
draftPost.should.exist;
|
||||
draftPost.title.should.eql(newTitle);
|
||||
draftPost.status = publishedState;
|
||||
testUtils.API.checkResponse(draftPost, 'post');
|
||||
|
||||
request.put(testUtils.API.getApiQuery('posts/' + draftPost.id + '/'))
|
||||
.set('X-CSRF-Token', csrfToken)
|
||||
.send(draftPost)
|
||||
.expect(200)
|
||||
.end(function (err, res) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
var publishedPost = res.body;
|
||||
|
||||
res.headers['x-cache-invalidate'].should.eql('/, /page/*, /rss/, /rss/*, /tag/*, /' + publishedPost.slug + '/');
|
||||
res.should.be.json;
|
||||
publishedPost.should.exist;
|
||||
publishedPost.title.should.eql(newTitle);
|
||||
publishedPost.status.should.eql(publishedState);
|
||||
testUtils.API.checkResponse(publishedPost, 'post');
|
||||
|
||||
request.put(testUtils.API.getApiQuery('posts/' + publishedPost.id + '/'))
|
||||
.set('X-CSRF-Token', csrfToken)
|
||||
.send(publishedPost)
|
||||
.expect(200)
|
||||
.end(function (err, res) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
var updatedPost = res.body;
|
||||
res.headers['x-cache-invalidate'].should.eql('/, /page/*, /rss/, /rss/*, /tag/*, /' + updatedPost.slug + '/');
|
||||
res.should.be.json;
|
||||
updatedPost.should.exist;
|
||||
updatedPost.title.should.eql(newTitle);
|
||||
testUtils.API.checkResponse(updatedPost, 'post');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
// ## edit
|
||||
describe('Edit', function () {
|
||||
it('can edit a post', function (done) {
|
||||
request.get(testUtils.API.getApiQuery('posts/1/'))
|
||||
.end(function (err, res) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
var jsonResponse = res.body,
|
||||
changedValue = 'My new Title';
|
||||
jsonResponse.should.exist;
|
||||
jsonResponse.title = changedValue;
|
||||
|
||||
request.put(testUtils.API.getApiQuery('posts/1/'))
|
||||
.set('X-CSRF-Token', csrfToken)
|
||||
.send(jsonResponse)
|
||||
.expect(200)
|
||||
.end(function (err, res) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
var putBody = res.body;
|
||||
res.headers['x-cache-invalidate'].should.eql('/, /page/*, /rss/, /rss/*, /tag/*, /' + putBody.slug + '/');
|
||||
res.should.be.json;
|
||||
putBody.should.exist;
|
||||
putBody.title.should.eql(changedValue);
|
||||
|
||||
testUtils.API.checkResponse(putBody, 'post');
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('can change a post to a static page', function (done) {
|
||||
request.get(testUtils.API.getApiQuery('posts/1/'))
|
||||
.end(function (err, res) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
var jsonResponse = res.body,
|
||||
changedValue = true;
|
||||
jsonResponse.should.exist;
|
||||
jsonResponse.page.should.eql(0);
|
||||
jsonResponse.page = changedValue;
|
||||
|
||||
request.put(testUtils.API.getApiQuery('posts/1/'))
|
||||
.set('X-CSRF-Token', csrfToken)
|
||||
.send(jsonResponse)
|
||||
.expect(200)
|
||||
.end(function (err, res) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
var putBody = res.body;
|
||||
res.headers['x-cache-invalidate'].should.eql('/, /page/*, /rss/, /rss/*, /tag/*, /' + putBody.slug + '/');
|
||||
res.should.be.json;
|
||||
putBody.should.exist;
|
||||
putBody.page.should.eql(changedValue);
|
||||
|
||||
testUtils.API.checkResponse(putBody, 'post');
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('can change a static page to a post', function (done) {
|
||||
request.get(testUtils.API.getApiQuery('posts/7/'))
|
||||
.end(function (err, res) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
var jsonResponse = res.body,
|
||||
changedValue = false;
|
||||
jsonResponse.should.exist;
|
||||
jsonResponse.page.should.eql(1);
|
||||
jsonResponse.page = changedValue;
|
||||
|
||||
request.put(testUtils.API.getApiQuery('posts/1/'))
|
||||
.set('X-CSRF-Token', csrfToken)
|
||||
.send(jsonResponse)
|
||||
.expect(200)
|
||||
.end(function (err, res) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
var putBody = res.body;
|
||||
res.headers['x-cache-invalidate'].should.eql('/, /page/*, /rss/, /rss/*, /tag/*, /' + putBody.slug + '/');
|
||||
res.should.be.json;
|
||||
putBody.should.exist;
|
||||
putBody.page.should.eql(changedValue);
|
||||
|
||||
testUtils.API.checkResponse(putBody, 'post');
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('can\'t edit a post with invalid CSRF token', function (done) {
|
||||
request.get(testUtils.API.getApiQuery('posts/1/'))
|
||||
.end(function (err, res) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
var jsonResponse = res.body;
|
||||
request.put(testUtils.API.getApiQuery('posts/1/'))
|
||||
.set('X-CSRF-Token', 'invalid-token')
|
||||
.send(jsonResponse)
|
||||
.expect(403)
|
||||
.end(function (err, res) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('published_at = null', function (done) {
|
||||
request.get(testUtils.API.getApiQuery('posts/1/'))
|
||||
.end(function (err, res) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
var jsonResponse = res.body,
|
||||
changedValue = 'My new Title';
|
||||
jsonResponse.should.exist;
|
||||
jsonResponse.title = changedValue;
|
||||
jsonResponse.published_at = null;
|
||||
|
||||
request.put(testUtils.API.getApiQuery('posts/1/'))
|
||||
.set('X-CSRF-Token', csrfToken)
|
||||
.send(jsonResponse)
|
||||
.expect(200)
|
||||
.end(function (err, res) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
var putBody = res.body;
|
||||
res.headers['x-cache-invalidate'].should.eql('/, /page/*, /rss/, /rss/*, /tag/*, /' + putBody.slug + '/');
|
||||
res.should.be.json;
|
||||
putBody.should.exist;
|
||||
putBody.title.should.eql(changedValue);
|
||||
if (_.isEmpty(putBody.published_at)) {
|
||||
should.fail('null', 'valid date', 'publish_at should not be empty');
|
||||
done();
|
||||
}
|
||||
testUtils.API.checkResponse(putBody, 'post');
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
// ## delete
|
||||
describe('Delete', function () {
|
||||
it('can\'t edit non existent post', function (done) {
|
||||
request.get(testUtils.API.getApiQuery('posts/1/'))
|
||||
.end(function (err, res) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
var jsonResponse = res.body,
|
||||
changedValue = 'My new Title';
|
||||
jsonResponse.title.exist;
|
||||
jsonResponse.testvalue = changedValue;
|
||||
jsonResponse.id = 99;
|
||||
request.put(testUtils.API.getApiQuery('posts/99/'))
|
||||
.set('X-CSRF-Token', csrfToken)
|
||||
.send(jsonResponse)
|
||||
.expect(404)
|
||||
.end(function (err, res) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
var putBody = res.body;
|
||||
should.not.exist(res.headers['x-cache-invalidate']);
|
||||
res.should.be.json;
|
||||
testUtils.API.checkResponseValue(putBody, ['error']);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('can delete a post', function (done) {
|
||||
var deletePostId = 1;
|
||||
request.del(testUtils.API.getApiQuery('posts/' + deletePostId + '/'))
|
||||
.set('X-CSRF-Token', csrfToken)
|
||||
.expect(200)
|
||||
.end(function (err, res) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
res.should.be.json;
|
||||
var jsonResponse = res.body;
|
||||
jsonResponse.should.exist;
|
||||
res.headers['x-cache-invalidate'].should.eql('/, /page/*, /rss/, /rss/*, /tag/*, /' + jsonResponse.slug + '/');
|
||||
testUtils.API.checkResponse(jsonResponse, 'post');
|
||||
jsonResponse.id.should.eql(deletePostId);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('can\'t delete a non existent post', function (done) {
|
||||
request.del(testUtils.API.getApiQuery('posts/99/'))
|
||||
.set('X-CSRF-Token', csrfToken)
|
||||
.expect(404)
|
||||
.end(function (err, res) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
should.not.exist(res.headers['x-cache-invalidate']);
|
||||
res.should.be.json;
|
||||
var jsonResponse = res.body;
|
||||
jsonResponse.should.exist;
|
||||
testUtils.API.checkResponseValue(jsonResponse, ['error']);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('can delete a new draft', function (done) {
|
||||
var newTitle = 'My Post',
|
||||
publishedState = 'draft',
|
||||
newPost = {status: publishedState, title: newTitle, markdown: 'my post'};
|
||||
|
||||
request.post(testUtils.API.getApiQuery('posts/'))
|
||||
.set('X-CSRF-Token', csrfToken)
|
||||
.send(newPost)
|
||||
.expect(200)
|
||||
.end(function (err ,res) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
var draftPost = res.body;
|
||||
|
||||
res.should.be.json;
|
||||
draftPost.should.exist;
|
||||
draftPost.title.should.eql(newTitle);
|
||||
draftPost.status = publishedState;
|
||||
testUtils.API.checkResponse(draftPost, 'post');
|
||||
|
||||
request.del(testUtils.API.getApiQuery('posts/' + draftPost.id + '/'))
|
||||
.set('X-CSRF-Token', csrfToken)
|
||||
.expect(200)
|
||||
.end(function (err, res) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
res.should.be.json;
|
||||
var jsonResponse = res.body
|
||||
jsonResponse.should.exist;
|
||||
testUtils.API.checkResponse(jsonResponse, 'post');
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('Dated Permalinks', function () {
|
||||
before(function (done) {
|
||||
request.get(testUtils.API.getApiQuery('settings/'))
|
||||
.end(function (err, res) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
var jsonResponse = res.body;
|
||||
jsonResponse.permalinks = '/:year/:month/:day/:slug/';
|
||||
|
||||
request.put(testUtils.API.getApiQuery('settings/'))
|
||||
.set('X-CSRF-Token', csrfToken)
|
||||
.send(jsonResponse)
|
||||
.end(function (err, res) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
after(function (done) {
|
||||
request.get(testUtils.API.getApiQuery('settings/'))
|
||||
.end(function (err, res) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
var jsonResponse = res.body;
|
||||
jsonResponse.permalinks = '/:slug/';
|
||||
|
||||
request.put(testUtils.API.getApiQuery('settings/'))
|
||||
.set('X-CSRF-Token', csrfToken)
|
||||
.send(jsonResponse)
|
||||
.end(function (err, res) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('Can read a post', function (done) {
|
||||
// nothing should have changed here
|
||||
request.get(testUtils.API.getApiQuery('posts/2/'))
|
||||
.expect(200)
|
||||
.end(function (err, res) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
should.not.exist(res.headers['x-cache-invalidate']);
|
||||
res.should.be.json;
|
||||
|
||||
var jsonResponse = res.body;
|
||||
jsonResponse.should.exist;
|
||||
testUtils.API.checkResponse(jsonResponse, 'post');
|
||||
jsonResponse.slug.should.not.match(/^\/[0-9]{4}\/[0-9]{2}\/[0-9]{2}/);
|
||||
jsonResponse.page.should.eql(0);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('Can edit a post', function (done) {
|
||||
request.get(testUtils.API.getApiQuery('posts/2/'))
|
||||
.end(function (err, res) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
var jsonResponse = res.body,
|
||||
changedValue = 'My new Title';
|
||||
jsonResponse.should.exist;
|
||||
jsonResponse.title = changedValue;
|
||||
|
||||
request.put(testUtils.API.getApiQuery('posts/2/'))
|
||||
.set('X-CSRF-Token', csrfToken)
|
||||
.send(jsonResponse)
|
||||
.expect(200)
|
||||
.end(function (err, res) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
var putBody = res.body;
|
||||
var today = new Date(),
|
||||
dd = ("0" + today.getDate()).slice(-2),
|
||||
mm = ("0" + (today.getMonth() + 1)).slice(-2),
|
||||
yyyy = today.getFullYear(),
|
||||
postLink = '/' + yyyy + '/' + mm + '/' + dd + '/' + putBody.slug + '/';
|
||||
|
||||
res.headers['x-cache-invalidate'].should.eql('/, /page/*, /rss/, /rss/*, /tag/*, ' + postLink);
|
||||
res.should.be.json;
|
||||
putBody.should.exist;
|
||||
putBody.title.should.eql(changedValue);
|
||||
|
||||
testUtils.API.checkResponse(putBody, 'post');
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
|
||||
|
||||
});
|
229
core/test/functional/routes/api/settings_test.js
Normal file
229
core/test/functional/routes/api/settings_test.js
Normal file
@ -0,0 +1,229 @@
|
||||
/*global describe, it, before, after */
|
||||
var supertest = require('supertest'),
|
||||
express = require('express'),
|
||||
should = require('should'),
|
||||
_ = require('lodash'),
|
||||
testUtils = require('../../../utils'),
|
||||
|
||||
ghost = require('../../../../../core'),
|
||||
|
||||
httpServer,
|
||||
request,
|
||||
agent;
|
||||
|
||||
|
||||
describe('Settings API', function () {
|
||||
var user = testUtils.DataGenerator.forModel.users[0],
|
||||
csrfToken = '';
|
||||
|
||||
before(function (done) {
|
||||
var app = express();
|
||||
|
||||
ghost({app: app}).then(function (_httpServer) {
|
||||
httpServer = _httpServer;
|
||||
// request = supertest(app);
|
||||
request = supertest.agent(app);
|
||||
|
||||
testUtils.clearData()
|
||||
.then(function () {
|
||||
return testUtils.initData();
|
||||
})
|
||||
.then(function () {
|
||||
return testUtils.insertDefaultFixtures();
|
||||
})
|
||||
.then(function () {
|
||||
|
||||
request.get('/ghost/signin/')
|
||||
.expect(200)
|
||||
.end(function (err, res) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
var pattern_meta = /<meta.*?name="csrf-param".*?content="(.*?)".*?>/i;
|
||||
pattern_meta.should.exist;
|
||||
csrfToken = res.text.match(pattern_meta)[1];
|
||||
|
||||
setTimeout(function () {
|
||||
request.post('/ghost/signin/')
|
||||
.set('X-CSRF-Token', csrfToken)
|
||||
.send({email: user.email, password: user.password})
|
||||
.expect(200)
|
||||
.end(function (err, res) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
|
||||
request.saveCookies(res);
|
||||
request.get('/ghost/')
|
||||
.expect(200)
|
||||
.end(function (err, res) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
// console.log('/ghost/', err, res);
|
||||
csrfToken = res.text.match(pattern_meta)[1];
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
}, 2000);
|
||||
|
||||
});
|
||||
}, done);
|
||||
}).otherwise(function (e) {
|
||||
console.log('Ghost Error: ', e);
|
||||
console.log(e.stack);
|
||||
});
|
||||
});
|
||||
|
||||
after(function () {
|
||||
httpServer.close();
|
||||
});
|
||||
|
||||
// TODO: currently includes values of type=core
|
||||
it('can retrieve all settings', function (done) {
|
||||
request.get(testUtils.API.getApiQuery('settings/'))
|
||||
.expect(200)
|
||||
.end(function (err, res) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
should.not.exist(res.headers['x-cache-invalidate']);
|
||||
res.should.be.json;
|
||||
var jsonResponse = res.body;
|
||||
jsonResponse.should.exist;
|
||||
|
||||
testUtils.API.checkResponse(jsonResponse, 'settings');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('can retrieve a setting', function (done) {
|
||||
request.get(testUtils.API.getApiQuery('settings/title/'))
|
||||
.expect(200)
|
||||
.end(function (err, res) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
should.not.exist(res.headers['x-cache-invalidate']);
|
||||
res.should.be.json;
|
||||
var jsonResponse = res.body;
|
||||
|
||||
jsonResponse.should.exist;
|
||||
testUtils.API.checkResponseValue(jsonResponse, ['key', 'value']);
|
||||
jsonResponse.key.should.eql('title');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('can\'t retrieve non existent setting', function (done) {
|
||||
request.get(testUtils.API.getApiQuery('settings/testsetting/'))
|
||||
.expect(404)
|
||||
.end(function (err, res) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
should.not.exist(res.headers['x-cache-invalidate']);
|
||||
res.should.be.json;
|
||||
var jsonResponse = res.body;
|
||||
jsonResponse.should.exist;
|
||||
testUtils.API.checkResponseValue(jsonResponse, ['error']);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('can edit settings', function (done) {
|
||||
request.get(testUtils.API.getApiQuery('settings/'))
|
||||
.end(function (err, res) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
var jsonResponse = res.body,
|
||||
changedValue = 'Ghost changed';
|
||||
jsonResponse.should.exist;
|
||||
jsonResponse.title = changedValue;
|
||||
|
||||
request.put(testUtils.API.getApiQuery('settings/'))
|
||||
.set('X-CSRF-Token', csrfToken)
|
||||
.send(jsonResponse)
|
||||
.expect(200)
|
||||
.end(function (err, res) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
var putBody = res.body;
|
||||
res.headers['x-cache-invalidate'].should.eql('/*');
|
||||
res.should.be.json;
|
||||
putBody.should.exist;
|
||||
putBody.title.should.eql(changedValue);
|
||||
testUtils.API.checkResponse(putBody, 'settings');
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('can\'t edit settings with invalid CSRF token', function (done) {
|
||||
request.get(testUtils.API.getApiQuery('settings'))
|
||||
.end(function (err, res) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
var jsonResponse = res.body,
|
||||
changedValue = 'Ghost changed';
|
||||
jsonResponse.should.exist;
|
||||
jsonResponse.title = changedValue;
|
||||
|
||||
request.put(testUtils.API.getApiQuery('settings/'))
|
||||
.set('X-CSRF-Token', 'invalid-token')
|
||||
.send(jsonResponse)
|
||||
.expect(403)
|
||||
.end(function (err, res) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('can\'t edit non existent setting', function (done) {
|
||||
request.get(testUtils.API.getApiQuery('settings'))
|
||||
.end(function (err, res) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
var jsonResponse = res.body,
|
||||
newValue = 'new value';
|
||||
jsonResponse.should.exist;
|
||||
jsonResponse.testvalue = newValue;
|
||||
|
||||
request.put(testUtils.API.getApiQuery('settings/'))
|
||||
.set('X-CSRF-Token', csrfToken)
|
||||
.send(jsonResponse)
|
||||
.expect(404)
|
||||
.end(function (err, res) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
var putBody = res.body;
|
||||
should.not.exist(res.headers['x-cache-invalidate']);
|
||||
res.should.be.json;
|
||||
testUtils.API.checkResponseValue(putBody, ['error']);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
});
|
103
core/test/functional/routes/api/tags_test.js
Normal file
103
core/test/functional/routes/api/tags_test.js
Normal file
@ -0,0 +1,103 @@
|
||||
/*global describe, it, before, after */
|
||||
var supertest = require('supertest'),
|
||||
express = require('express'),
|
||||
should = require('should'),
|
||||
_ = require('lodash'),
|
||||
testUtils = require('../../../utils'),
|
||||
|
||||
ghost = require('../../../../../core'),
|
||||
|
||||
httpServer,
|
||||
request,
|
||||
agent;
|
||||
|
||||
|
||||
describe('Tag API', function () {
|
||||
var user = testUtils.DataGenerator.forModel.users[0],
|
||||
csrfToken = '';
|
||||
|
||||
before(function (done) {
|
||||
var app = express();
|
||||
|
||||
ghost({app: app}).then(function (_httpServer) {
|
||||
httpServer = _httpServer;
|
||||
// request = supertest(app);
|
||||
request = supertest.agent(app);
|
||||
|
||||
testUtils.clearData()
|
||||
.then(function () {
|
||||
return testUtils.initData();
|
||||
})
|
||||
.then(function () {
|
||||
return testUtils.insertDefaultFixtures();
|
||||
})
|
||||
.then(function () {
|
||||
|
||||
request.get('/ghost/signin/')
|
||||
.expect(200)
|
||||
.end(function (err, res) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
var pattern_meta = /<meta.*?name="csrf-param".*?content="(.*?)".*?>/i;
|
||||
pattern_meta.should.exist;
|
||||
csrfToken = res.text.match(pattern_meta)[1];
|
||||
|
||||
setTimeout(function () {
|
||||
request.post('/ghost/signin/')
|
||||
.set('X-CSRF-Token', csrfToken)
|
||||
.send({email: user.email, password: user.password})
|
||||
.expect(200)
|
||||
.end(function (err, res) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
|
||||
request.saveCookies(res);
|
||||
request.get('/ghost/')
|
||||
.expect(200)
|
||||
.end(function (err, res) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
csrfToken = res.text.match(pattern_meta)[1];
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
}, 2000);
|
||||
|
||||
});
|
||||
}, done);
|
||||
}).otherwise(function (e) {
|
||||
console.log('Ghost Error: ', e);
|
||||
console.log(e.stack);
|
||||
});
|
||||
});
|
||||
|
||||
after(function () {
|
||||
httpServer.close();
|
||||
});
|
||||
|
||||
it('can retrieve all tags', function (done) {
|
||||
request.get(testUtils.API.getApiQuery('tags/'))
|
||||
.expect(200)
|
||||
.end(function (err, res) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
should.not.exist(res.headers['x-cache-invalidate']);
|
||||
res.should.be.json;
|
||||
var jsonResponse = res.body;
|
||||
jsonResponse.should.exist;
|
||||
jsonResponse.should.have.length(6);
|
||||
testUtils.API.checkResponse(jsonResponse[0], 'tag');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
});
|
199
core/test/functional/routes/api/users_test.js
Normal file
199
core/test/functional/routes/api/users_test.js
Normal file
@ -0,0 +1,199 @@
|
||||
/*global describe, it, before, after */
|
||||
var supertest = require('supertest'),
|
||||
express = require('express'),
|
||||
should = require('should'),
|
||||
_ = require('lodash'),
|
||||
testUtils = require('../../../utils'),
|
||||
|
||||
ghost = require('../../../../../core'),
|
||||
|
||||
httpServer,
|
||||
request,
|
||||
agent;
|
||||
|
||||
|
||||
describe('User API', function () {
|
||||
var user = testUtils.DataGenerator.forModel.users[0],
|
||||
csrfToken = '';
|
||||
|
||||
before(function (done) {
|
||||
var app = express();
|
||||
|
||||
ghost({app: app}).then(function (_httpServer) {
|
||||
httpServer = _httpServer;
|
||||
// request = supertest(app);
|
||||
request = supertest.agent(app);
|
||||
|
||||
testUtils.clearData()
|
||||
.then(function () {
|
||||
return testUtils.initData();
|
||||
})
|
||||
.then(function () {
|
||||
return testUtils.insertDefaultFixtures();
|
||||
})
|
||||
.then(function () {
|
||||
|
||||
request.get('/ghost/signin/')
|
||||
.expect(200)
|
||||
.end(function (err, res) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
var pattern_meta = /<meta.*?name="csrf-param".*?content="(.*?)".*?>/i;
|
||||
pattern_meta.should.exist;
|
||||
csrfToken = res.text.match(pattern_meta)[1];
|
||||
|
||||
setTimeout(function () {
|
||||
request.post('/ghost/signin/')
|
||||
.set('X-CSRF-Token', csrfToken)
|
||||
.send({email: user.email, password: user.password})
|
||||
.expect(200)
|
||||
.end(function (err, res) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
request.saveCookies(res);
|
||||
request.get('/ghost/')
|
||||
.expect(200)
|
||||
.end(function (err, res) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
// console.log('/ghost/', err, res);
|
||||
csrfToken = res.text.match(pattern_meta)[1];
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
}, 2000);
|
||||
|
||||
});
|
||||
}, done);
|
||||
}).otherwise(function (e) {
|
||||
console.log('Ghost Error: ', e);
|
||||
console.log(e.stack);
|
||||
});
|
||||
});
|
||||
|
||||
after(function () {
|
||||
httpServer.close();
|
||||
});
|
||||
|
||||
it('can retrieve all users', function (done) {
|
||||
request.get(testUtils.API.getApiQuery('users/'))
|
||||
.expect(200)
|
||||
.end(function (err, res) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
should.not.exist(res.headers['x-cache-invalidate']);
|
||||
res.should.be.json;
|
||||
var jsonResponse = res.body;
|
||||
jsonResponse[0].should.exist;
|
||||
|
||||
testUtils.API.checkResponse(jsonResponse[0], 'user');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('can retrieve a user', function (done) {
|
||||
request.get(testUtils.API.getApiQuery('users/me/'))
|
||||
.expect(200)
|
||||
.end(function (err, res) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
should.not.exist(res.headers['x-cache-invalidate']);
|
||||
res.should.be.json;
|
||||
var jsonResponse = res.body;
|
||||
jsonResponse.should.exist;
|
||||
|
||||
testUtils.API.checkResponse(jsonResponse, 'user');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('can\'t retrieve non existent user', function (done) {
|
||||
request.get(testUtils.API.getApiQuery('users/99/'))
|
||||
.expect(404)
|
||||
.end(function (err, res) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
should.not.exist(res.headers['x-cache-invalidate']);
|
||||
res.should.be.json;
|
||||
var jsonResponse = res.body;
|
||||
jsonResponse.should.exist;
|
||||
|
||||
testUtils.API.checkResponseValue(jsonResponse, ['error']);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('can edit a user', function (done) {
|
||||
request.get(testUtils.API.getApiQuery('users/me/'))
|
||||
.end(function (err, res) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
var jsonResponse = res.body,
|
||||
changedValue = 'joe-bloggs.ghost.org';
|
||||
jsonResponse.should.exist;
|
||||
jsonResponse.website = changedValue;
|
||||
|
||||
request.put(testUtils.API.getApiQuery('users/me/'))
|
||||
.set('X-CSRF-Token', csrfToken)
|
||||
.send(jsonResponse)
|
||||
.expect(200)
|
||||
.end(function (err, res) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
var putBody = res.body;
|
||||
res.headers['x-cache-invalidate'].should.eql('/*');
|
||||
res.should.be.json;
|
||||
putBody.should.exist;
|
||||
putBody.website.should.eql(changedValue);
|
||||
|
||||
testUtils.API.checkResponse(putBody, 'user');
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('can\'t edit a user with invalid CSRF token', function (done) {
|
||||
request.get(testUtils.API.getApiQuery('users/me/'))
|
||||
.end(function (err, res) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
var jsonResponse = res.body,
|
||||
changedValue = 'joe-bloggs.ghost.org';
|
||||
jsonResponse.should.exist;
|
||||
jsonResponse.website = changedValue;
|
||||
|
||||
request.put(testUtils.API.getApiQuery('users/me/'))
|
||||
.set('X-CSRF-Token', 'invalid-token')
|
||||
.send(jsonResponse)
|
||||
.expect(403)
|
||||
.end(function (err, res) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
});
|
@ -6,11 +6,13 @@
|
||||
// But then again testing real code, rather than mock code, might be more useful...
|
||||
|
||||
var request = require('supertest'),
|
||||
express = require('express'),
|
||||
should = require('should'),
|
||||
moment = require('moment'),
|
||||
|
||||
testUtils = require('../../utils'),
|
||||
config = require('../../../server/config'),
|
||||
ghost = require('../../../../core'),
|
||||
httpServer,
|
||||
|
||||
ONE_HOUR_S = 60 * 60,
|
||||
ONE_YEAR_S = 365 * 24 * ONE_HOUR_S,
|
||||
@ -38,15 +40,26 @@ describe('Frontend Routing', function () {
|
||||
}
|
||||
|
||||
before(function (done) {
|
||||
testUtils.clearData().then(function () {
|
||||
// we initialise data, but not a user. No user should be required for navigating the frontend
|
||||
return testUtils.initData();
|
||||
}).then(function () {
|
||||
done();
|
||||
}, done);
|
||||
var app = express();
|
||||
|
||||
// Setup the request object with the correct URL
|
||||
request = request(config().url);
|
||||
ghost({app: app}).then(function (_httpServer) {
|
||||
// Setup the request object with the ghost express app
|
||||
httpServer = _httpServer;
|
||||
request = request(app);
|
||||
testUtils.clearData().then(function () {
|
||||
// we initialise data, but not a user. No user should be required for navigating the frontend
|
||||
return testUtils.initData();
|
||||
}).then(function () {
|
||||
done();
|
||||
}, done);
|
||||
}).otherwise(function (e) {
|
||||
console.log('Ghost Error: ', e);
|
||||
console.log(e.stack);
|
||||
});
|
||||
});
|
||||
|
||||
after(function () {
|
||||
httpServer.close();
|
||||
});
|
||||
|
||||
describe('Home', function () {
|
||||
@ -129,6 +142,14 @@ describe('Frontend Routing', function () {
|
||||
.expect(302)
|
||||
.end(doEnd(done));
|
||||
});
|
||||
|
||||
it('should get redirected to /rss/ from /feed/', function (done) {
|
||||
request.get('/feed/')
|
||||
.expect('Location', '/rss/')
|
||||
.expect('Cache-Control', cacheRules.year)
|
||||
.expect(301)
|
||||
.end(doEnd(done));
|
||||
});
|
||||
});
|
||||
|
||||
// ### The rest of the tests require more data
|
||||
|
53
core/test/integration/api/api_db_spec.js
Normal file
53
core/test/integration/api/api_db_spec.js
Normal file
@ -0,0 +1,53 @@
|
||||
/*globals describe, before, beforeEach, afterEach, it */
|
||||
var testUtils = require('../../utils'),
|
||||
should = require('should'),
|
||||
|
||||
// Stuff we are testing
|
||||
DataGenerator = require('../../utils/fixtures/data-generator'),
|
||||
dbAPI = require('../../../server/api/db');
|
||||
TagsAPI = require('../../../server/api/tags');
|
||||
PostAPI = require('../../../server/api/posts');
|
||||
|
||||
describe('DB API', function () {
|
||||
|
||||
before(function (done) {
|
||||
testUtils.clearData().then(function () {
|
||||
done();
|
||||
}, done);
|
||||
});
|
||||
|
||||
beforeEach(function (done) {
|
||||
testUtils.initData()
|
||||
.then(function () {
|
||||
return testUtils.insertDefaultFixtures();
|
||||
})
|
||||
.then(function () {
|
||||
done();
|
||||
}, done);
|
||||
});
|
||||
|
||||
afterEach(function (done) {
|
||||
testUtils.clearData().then(function () {
|
||||
done();
|
||||
}, done);
|
||||
});
|
||||
|
||||
it('delete all content', function (done) {
|
||||
|
||||
dbAPI.deleteAllContent().then(function (result){
|
||||
should.exist(result.message);
|
||||
result.message.should.equal('Successfully deleted all content from your blog.')
|
||||
}).then(function () {
|
||||
TagsAPI.browse().then(function (results) {
|
||||
should.exist(results);
|
||||
results.length.should.equal(0);
|
||||
});
|
||||
}).then(function () {
|
||||
PostAPI.browse().then(function (results) {
|
||||
should.exist(results);
|
||||
results.posts.length.should.equal(0);
|
||||
done();
|
||||
});
|
||||
}).then(null, done);
|
||||
});
|
||||
});
|
@ -419,7 +419,7 @@ describe('Post Model', function () {
|
||||
}).then(function (paginationResult) {
|
||||
paginationResult.page.should.equal(2);
|
||||
paginationResult.limit.should.equal(15);
|
||||
paginationResult.posts.length.should.equal(9);
|
||||
paginationResult.posts.length.should.equal(11);
|
||||
paginationResult.pages.should.equal(2);
|
||||
paginationResult.aspect.tag.name.should.equal('injection');
|
||||
paginationResult.aspect.tag.slug.should.equal('injection');
|
||||
|
@ -65,7 +65,7 @@ describe('Settings Model', function () {
|
||||
|
||||
should.exist(found);
|
||||
|
||||
found.attributes.value.should.equal(firstSetting.attributes.value);
|
||||
found.get('value').should.equal(firstSetting.attributes.value);
|
||||
|
||||
done();
|
||||
|
||||
@ -206,7 +206,9 @@ describe('Settings Model', function () {
|
||||
return SettingsModel.findAll();
|
||||
}).then(function (allSettings) {
|
||||
allSettings.length.should.be.above(0);
|
||||
return SettingsModel.read('description');
|
||||
return SettingsModel.read('description').then(function (descriptionSetting) {
|
||||
return descriptionSetting;
|
||||
});
|
||||
}).then(function (descriptionSetting) {
|
||||
// Testing against the actual value in default-settings.json feels icky,
|
||||
// but it's easier to fix the test if that ever changes than to mock out that behaviour
|
||||
|
@ -177,9 +177,13 @@ describe('Tag Model', function () {
|
||||
return PostModel.read({id: postModel.id, status: 'all'}, { withRelated: ['tags']});
|
||||
}).then(function (reloadedPost) {
|
||||
var tagModels = reloadedPost.related('tags').models,
|
||||
tagNames = tagModels.map(function (t) { return t.attributes.name; });
|
||||
tagNames = tagModels.map(function (t) { return t.attributes.name; }),
|
||||
tagIds = _.pluck(tagModels, 'id');
|
||||
tagNames.sort().should.eql(['tag1', 'tag2', 'tag3']);
|
||||
tagModels[2].id.should.eql(4); // make sure it hasn't just added a new tag with the same name
|
||||
|
||||
// make sure it hasn't just added a new tag with the same name
|
||||
// Don't expect a certain order in results - check for number of items!
|
||||
Math.max.apply(Math, tagIds).should.eql(4);
|
||||
|
||||
done();
|
||||
}).then(null, done);
|
||||
@ -248,9 +252,14 @@ describe('Tag Model', function () {
|
||||
return PostModel.read({id: postModel.id, status: 'all'}, { withRelated: ['tags']});
|
||||
}).then(function (reloadedPost) {
|
||||
var tagModels = reloadedPost.related('tags').models,
|
||||
tagNames = tagModels.map(function (t) { return t.attributes.name; });
|
||||
tagNames = tagModels.map(function (t) { return t.attributes.name; }),
|
||||
tagIds = _.pluck(tagModels, 'id');
|
||||
|
||||
tagNames.sort().should.eql(['tag1', 'tag2', 'tag3']);
|
||||
tagModels[2].id.should.eql(4); // make sure it hasn't just added a new tag with the same name
|
||||
|
||||
// make sure it hasn't just added a new tag with the same name
|
||||
// Don't expect a certain order in results - check for number of items!
|
||||
Math.max.apply(Math, tagIds).should.eql(4);
|
||||
|
||||
done();
|
||||
}).then(null, done);
|
||||
@ -279,9 +288,14 @@ describe('Tag Model', function () {
|
||||
return PostModel.read({id: postModel.id, status: 'all'}, { withRelated: ['tags']});
|
||||
}).then(function (reloadedPost) {
|
||||
var tagModels = reloadedPost.related('tags').models,
|
||||
tagNames = tagModels.map(function (t) { return t.attributes.name; });
|
||||
tagNames = tagModels.map(function (t) { return t.get('name'); }),
|
||||
tagIds = _.pluck(tagModels, 'id');
|
||||
|
||||
tagNames.sort().should.eql(['tag1', 'tag2', 'tag3', 'tag4']);
|
||||
tagModels[2].id.should.eql(4); // make sure it hasn't just added a new tag with the same name
|
||||
|
||||
// make sure it hasn't just added a new tag with the same name
|
||||
// Don't expect a certain order in results - check for number of items!
|
||||
Math.max.apply(Math, tagIds).should.eql(5);
|
||||
|
||||
done();
|
||||
}).then(null, done);
|
||||
|
@ -141,6 +141,15 @@ describe('User Model', function run() {
|
||||
}, done);
|
||||
});
|
||||
|
||||
it('sets last login time on successful login', function (done) {
|
||||
var userData = testUtils.DataGenerator.forModel.users[0];
|
||||
|
||||
UserModel.check({email: userData.email, pw:userData.password}).then(function (activeUser) {
|
||||
should.exist(activeUser.get('last_login'));
|
||||
done();
|
||||
}).then(null, done);
|
||||
});
|
||||
|
||||
it('can\'t add second', function (done) {
|
||||
var userData = testUtils.DataGenerator.forModel.users[1];
|
||||
|
||||
|
@ -3,16 +3,16 @@ var testUtils = require('../utils'),
|
||||
should = require('should'),
|
||||
when = require('when'),
|
||||
sinon = require('sinon'),
|
||||
express = require("express"),
|
||||
express = require('express'),
|
||||
rewire = require('rewire')
|
||||
|
||||
// Stuff we are testing
|
||||
colors = require("colors"),
|
||||
errors = require('../../server/errorHandling'),
|
||||
colors = require('colors'),
|
||||
errors = rewire('../../server/errorHandling'),
|
||||
// storing current environment
|
||||
currentEnv = process.env.NODE_ENV,
|
||||
ONE_HOUR_S = 60 * 60;
|
||||
currentEnv = process.env.NODE_ENV;
|
||||
|
||||
describe("Error handling", function () {
|
||||
describe('Error handling', function () {
|
||||
|
||||
// Just getting rid of jslint unused error
|
||||
should.exist(errors);
|
||||
@ -49,7 +49,7 @@ describe("Error handling", function () {
|
||||
var logStub;
|
||||
|
||||
beforeEach(function () {
|
||||
logStub = sinon.stub(console, "error");
|
||||
logStub = sinon.stub(console, 'error');
|
||||
// give environment a value that will console log
|
||||
process.env.NODE_ENV = "development";
|
||||
});
|
||||
@ -60,142 +60,115 @@ describe("Error handling", function () {
|
||||
process.env.NODE_ENV = currentEnv;
|
||||
});
|
||||
|
||||
it("logs errors from error objects", function () {
|
||||
var err = new Error("test1");
|
||||
it('logs errors from error objects', function () {
|
||||
var err = new Error('test1');
|
||||
|
||||
errors.logError(err);
|
||||
|
||||
// Calls log with message on Error objects
|
||||
logStub.calledThrice.should.be.true;
|
||||
logStub.firstCall.calledWith("\nERROR:".red, err.message.red).should.be.true;
|
||||
logStub.secondCall.calledWith('').should.be.true;
|
||||
logStub.thirdCall.calledWith(err.stack, '\n').should.be.true;
|
||||
|
||||
logStub.calledOnce.should.be.true;
|
||||
logStub.calledWith('\nERROR:'.red, err.message.red, '\n', '\n', err.stack, '\n').should.be.true;
|
||||
});
|
||||
|
||||
it("logs errors from strings", function () {
|
||||
var err = "test2";
|
||||
it('logs errors from strings', function () {
|
||||
var err = 'test2';
|
||||
|
||||
errors.logError(err);
|
||||
|
||||
// Calls log with string on strings
|
||||
logStub.calledTwice.should.be.true;
|
||||
logStub.firstCall.calledWith("\nERROR:".red, err.red).should.be.true;
|
||||
logStub.secondCall.calledWith('').should.be.true;
|
||||
logStub.calledOnce.should.be.true;
|
||||
logStub.calledWith('\nERROR:'.red, err.red, '\n').should.be.true;
|
||||
});
|
||||
|
||||
it("logs errors from an error object and two string arguments", function () {
|
||||
var err = new Error("test1"),
|
||||
message = "Testing";
|
||||
it('logs errors from an error object and two string arguments', function () {
|
||||
var err = new Error('test1'),
|
||||
message = 'Testing';
|
||||
|
||||
errors.logError(err, message, message);
|
||||
|
||||
// Calls log with message on Error objects
|
||||
|
||||
logStub.callCount.should.equal(5);
|
||||
logStub.calledWith("\nERROR:".red, err.message.red).should.be.true;
|
||||
logStub.firstCall.calledWith("\nERROR:".red, err.message.red).should.be.true;
|
||||
logStub.secondCall.calledWith(message.white).should.be.true;
|
||||
logStub.thirdCall.calledWith(message.green).should.be.true;
|
||||
logStub.getCall(3).calledWith('').should.be.true; //nth call uses zero-based numbering
|
||||
logStub.lastCall.calledWith(err.stack, '\n').should.be.true;
|
||||
logStub.calledOnce.should.be.true;
|
||||
logStub.calledWith('\nERROR:'.red, err.message.red, '\n', message.white, '\n', message.green, '\n', err.stack, '\n');
|
||||
});
|
||||
|
||||
it("logs errors from three string arguments", function () {
|
||||
var message = "Testing";
|
||||
it('logs errors from three string arguments', function () {
|
||||
var message = 'Testing';
|
||||
|
||||
errors.logError(message, message, message);
|
||||
|
||||
// Calls log with message on Error objects
|
||||
|
||||
logStub.callCount.should.equal(4);
|
||||
logStub.firstCall.calledWith("\nERROR:".red, message.red).should.be.true;
|
||||
logStub.secondCall.calledWith(message.white).should.be.true;
|
||||
logStub.thirdCall.calledWith(message.green).should.be.true;
|
||||
logStub.lastCall.calledWith('').should.be.true;
|
||||
logStub.calledOnce.should.be.true;
|
||||
logStub.calledWith('\nERROR:'.red, message.red, '\n', message.white, '\n', message.green, '\n').should.be.true;
|
||||
});
|
||||
|
||||
it("logs errors from an undefined error argument", function () {
|
||||
var message = "Testing";
|
||||
it('logs errors from an undefined error argument', function () {
|
||||
var message = 'Testing';
|
||||
|
||||
errors.logError(undefined, message, message);
|
||||
|
||||
// Calls log with message on Error objects
|
||||
|
||||
logStub.callCount.should.equal(4);
|
||||
logStub.firstCall.calledWith("\nERROR:".red, "An unknown error occurred.".red).should.be.true;
|
||||
logStub.secondCall.calledWith(message.white).should.be.true;
|
||||
logStub.thirdCall.calledWith(message.green).should.be.true;
|
||||
logStub.lastCall.calledWith('').should.be.true;
|
||||
logStub.calledOnce.should.be.true;
|
||||
logStub.calledWith('\nERROR:'.red, 'An unknown error occurred.'.red, '\n', message.white, '\n', message.green , '\n').should.be.true;
|
||||
});
|
||||
|
||||
it("logs errors from an undefined context argument", function () {
|
||||
var message = "Testing";
|
||||
it('logs errors from an undefined context argument', function () {
|
||||
var message = 'Testing';
|
||||
|
||||
errors.logError(message, undefined, message);
|
||||
|
||||
// Calls log with message on Error objects
|
||||
|
||||
logStub.callCount.should.equal(3);
|
||||
logStub.firstCall.calledWith("\nERROR:".red, message.red).should.be.true;
|
||||
logStub.secondCall.calledWith(message.green).should.be.true;
|
||||
logStub.lastCall.calledWith('').should.be.true;
|
||||
logStub.calledOnce.should.be.true;
|
||||
logStub.calledWith('\nERROR:'.red, message.red, '\n', message.green, '\n').should.be.true;
|
||||
});
|
||||
|
||||
it("logs errors from an undefined help argument", function () {
|
||||
var message = "Testing";
|
||||
it('logs errors from an undefined help argument', function () {
|
||||
var message = 'Testing';
|
||||
|
||||
errors.logError(message, message, undefined);
|
||||
|
||||
// Calls log with message on Error objects
|
||||
|
||||
logStub.callCount.should.equal(3);
|
||||
logStub.firstCall.calledWith("\nERROR:".red, message.red).should.be.true;
|
||||
logStub.secondCall.calledWith(message.white).should.be.true;
|
||||
logStub.lastCall.calledWith('').should.be.true;
|
||||
logStub.calledOnce.should.be.true;
|
||||
logStub.calledWith('\nERROR:'.red, message.red, '\n', message.white, '\n').should.be.true;
|
||||
});
|
||||
|
||||
it("logs errors from a null error argument", function () {
|
||||
var message = "Testing";
|
||||
it('logs errors from a null error argument', function () {
|
||||
var message = 'Testing';
|
||||
|
||||
errors.logError(null, message, message);
|
||||
|
||||
// Calls log with message on Error objects
|
||||
|
||||
logStub.callCount.should.equal(4);
|
||||
logStub.firstCall.calledWith("\nERROR:".red, "An unknown error occurred.".red).should.be.true;
|
||||
logStub.secondCall.calledWith(message.white).should.be.true;
|
||||
logStub.thirdCall.calledWith(message.green).should.be.true;
|
||||
logStub.lastCall.calledWith('').should.be.true;
|
||||
logStub.calledOnce.should.be.true;
|
||||
logStub.calledWith('\nERROR:'.red, 'An unknown error occurred.'.red, '\n', message.white, '\n', message.green, '\n').should.be.true;
|
||||
});
|
||||
|
||||
it("logs errors from a null context argument", function () {
|
||||
var message = "Testing";
|
||||
it('logs errors from a null context argument', function () {
|
||||
var message = 'Testing';
|
||||
|
||||
errors.logError(message, null, message);
|
||||
|
||||
// Calls log with message on Error objects
|
||||
|
||||
logStub.callCount.should.equal(3);
|
||||
logStub.firstCall.calledWith("\nERROR:".red, message.red).should.be.true;
|
||||
logStub.secondCall.calledWith(message.green).should.be.true;
|
||||
logStub.lastCall.calledWith('').should.be.true;
|
||||
logStub.calledOnce.should.be.true;
|
||||
logStub.firstCall.calledWith('\nERROR:'.red, message.red, '\n', message.green, '\n').should.be.true;
|
||||
});
|
||||
|
||||
it("logs errors from a null help argument", function () {
|
||||
var message = "Testing";
|
||||
it('logs errors from a null help argument', function () {
|
||||
var message = 'Testing';
|
||||
|
||||
errors.logError(message, message, null);
|
||||
|
||||
// Calls log with message on Error objects
|
||||
|
||||
logStub.callCount.should.equal(3);
|
||||
logStub.firstCall.calledWith("\nERROR:".red, message.red).should.be.true;
|
||||
logStub.secondCall.calledWith(message.white).should.be.true;
|
||||
logStub.lastCall.calledWith('').should.be.true;
|
||||
logStub.calledOnce.should.be.true;
|
||||
logStub.firstCall.calledWith('\nERROR:'.red, message.red, '\n', message.white, '\n').should.be.true;
|
||||
|
||||
});
|
||||
|
||||
it("logs promise errors and redirects", function (done) {
|
||||
it('logs promise errors and redirects', function (done) {
|
||||
var def = when.defer(),
|
||||
prom = def.promise,
|
||||
req = null,
|
||||
@ -204,15 +177,15 @@ describe("Error handling", function () {
|
||||
return;
|
||||
}
|
||||
},
|
||||
redirectStub = sinon.stub(res, "redirect");
|
||||
redirectStub = sinon.stub(res, 'redirect');
|
||||
|
||||
// give environment a value that will console log
|
||||
prom.then(function () {
|
||||
throw new Error("Ran success handler");
|
||||
}, errors.logErrorWithRedirect("test1", null, null, "/testurl", req, res));
|
||||
throw new Error('Ran success handler');
|
||||
}, errors.logErrorWithRedirect('test1', null, null, '/testurl', req, res));
|
||||
|
||||
prom.otherwise(function () {
|
||||
logStub.calledWith("\nERROR:".red, "test1".red).should.equal(true);
|
||||
logStub.calledWith('\nERROR:'.red, 'test1'.red).should.equal(true);
|
||||
logStub.restore();
|
||||
|
||||
redirectStub.calledWith('/testurl').should.equal(true);
|
||||
@ -225,10 +198,31 @@ describe("Error handling", function () {
|
||||
});
|
||||
|
||||
describe('Rendering', function () {
|
||||
var sandbox;
|
||||
var sandbox,
|
||||
originalConfig;
|
||||
|
||||
before(function () {
|
||||
errors.updateActiveTheme('casper', false);
|
||||
originalConfig = errors.__get__('config');
|
||||
errors.__set__('config', function () {
|
||||
return {
|
||||
'paths': {
|
||||
'themePath': '/content/themes',
|
||||
'availableThemes': {
|
||||
'casper': {
|
||||
'assets': null,
|
||||
'default.hbs': '/content/themes/casper/default.hbs',
|
||||
'index.hbs': '/content/themes/casper/index.hbs',
|
||||
'page.hbs': '/content/themes/casper/page.hbs',
|
||||
'tag.hbs': '/content/themes/casper/tag.hbs'
|
||||
},
|
||||
'theme-with-error': {
|
||||
'error.hbs':''
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
errors.updateActiveTheme('casper');
|
||||
});
|
||||
|
||||
beforeEach(function () {
|
||||
@ -239,6 +233,9 @@ describe("Error handling", function () {
|
||||
sandbox.restore();
|
||||
});
|
||||
|
||||
after(function () {
|
||||
errors.__set__('config', originalConfig);
|
||||
});
|
||||
|
||||
it('Renders end-of-middleware 404 errors correctly', function (done) {
|
||||
var req = {method: 'GET'},
|
||||
@ -349,5 +346,26 @@ describe("Error handling", function () {
|
||||
err.code = 500;
|
||||
errors.error500(err, req, res, null);
|
||||
});
|
||||
|
||||
it('Renders custom error template if one exists', function(done){
|
||||
var code = 404,
|
||||
error = {message:'Custom view test'},
|
||||
req = {
|
||||
session: null
|
||||
},
|
||||
res = {
|
||||
status: function(code) {
|
||||
return this;
|
||||
},
|
||||
render: function(view, model, fn){
|
||||
view.should.eql('error');
|
||||
errors.updateActiveTheme('casper');
|
||||
done();
|
||||
}
|
||||
},
|
||||
next = null;
|
||||
errors.updateActiveTheme('theme-with-error');
|
||||
errors.renderErrorPage(code, error, req, res, next);
|
||||
})
|
||||
});
|
||||
});
|
||||
|
@ -296,18 +296,24 @@ describe('Core Helpers', function () {
|
||||
it('can render class string for context', function (done) {
|
||||
when.all([
|
||||
helpers.body_class.call({relativeUrl: '/'}),
|
||||
helpers.body_class.call({relativeUrl: '/a-post-title'}),
|
||||
helpers.body_class.call({relativeUrl: '/page/4'})
|
||||
helpers.body_class.call({relativeUrl: '/a-post-title', post: {}}),
|
||||
helpers.body_class.call({relativeUrl: '/page/4'}),
|
||||
helpers.body_class.call({relativeUrl: '/tag/foo', tag: { slug: 'foo'}}),
|
||||
helpers.body_class.call({relativeUrl: '/tag/foo/page/2', tag: { slug: 'foo'}})
|
||||
]).then(function (rendered) {
|
||||
rendered.length.should.equal(3);
|
||||
rendered.length.should.equal(5);
|
||||
|
||||
should.exist(rendered[0]);
|
||||
should.exist(rendered[1]);
|
||||
should.exist(rendered[2]);
|
||||
should.exist(rendered[3]);
|
||||
should.exist(rendered[4]);
|
||||
|
||||
rendered[0].string.should.equal('home-template');
|
||||
rendered[1].string.should.equal('post-template');
|
||||
rendered[2].string.should.equal('archive-template');
|
||||
rendered[3].string.should.equal('tag-template tag-foo');
|
||||
rendered[4].string.should.equal('archive-template tag-template tag-foo');
|
||||
|
||||
done();
|
||||
}).then(null, done);
|
||||
@ -446,8 +452,7 @@ describe('Core Helpers', function () {
|
||||
should.exist(handlebars.helpers.ghost_foot);
|
||||
});
|
||||
|
||||
it('returns meta tag string', function (done) {
|
||||
|
||||
it('outputs correct jquery for development mode', function (done) {
|
||||
helpers.assetHash = 'abc';
|
||||
|
||||
helpers.ghost_foot.call().then(function (rendered) {
|
||||
@ -457,6 +462,18 @@ describe('Core Helpers', function () {
|
||||
done();
|
||||
}).then(null, done);
|
||||
});
|
||||
|
||||
it('outputs correct jquery for production mode', function (done) {
|
||||
helpers.assetHash = 'abc';
|
||||
helpers.__set__('isProduction', true);
|
||||
|
||||
helpers.ghost_foot.call().then(function (rendered) {
|
||||
should.exist(rendered);
|
||||
rendered.string.should.match(/<script src=".*\/public\/jquery.min.js\?v=abc"><\/script>/);
|
||||
|
||||
done();
|
||||
}).then(null, done);
|
||||
});
|
||||
});
|
||||
|
||||
describe('has Block Helper', function () {
|
||||
|
@ -5,14 +5,15 @@
|
||||
*/
|
||||
|
||||
/*globals describe, it */
|
||||
var testUtils = require('../utils'),
|
||||
should = require('should'),
|
||||
var testUtils = require('../utils'),
|
||||
should = require('should'),
|
||||
|
||||
// Stuff we are testing
|
||||
Showdown = require('showdown'),
|
||||
github = require('../../shared/lib/showdown/extensions/github'),
|
||||
ghostdown = require('../../clientold/assets/lib/showdown/extensions/ghostdown'),
|
||||
converter = new Showdown.converter({extensions: [ghostdown, github]});
|
||||
Showdown = require('showdown'),
|
||||
ghostgfm = require('../../shared/lib/showdown/extensions/ghostgfm'),
|
||||
ghostimagepreview = require('../../shared/lib/showdown/extensions/ghostimagepreview'),
|
||||
|
||||
converter = new Showdown.converter({extensions: [ghostimagepreview, ghostgfm]});
|
||||
|
||||
describe("Showdown client side converter", function () {
|
||||
/*jslint regexp: true */
|
||||
@ -107,7 +108,7 @@ describe("Showdown client side converter", function () {
|
||||
it("should turn newlines into br tags in simple cases", function () {
|
||||
var testPhrases = [
|
||||
{input: "fizz\nbuzz", output: /^<p>fizz <br \/>\nbuzz<\/p>$/},
|
||||
{input: "Hello world\nIt's a fine day", output: /^<p>Hello world <br \/>\nIt\'s a fine day<\/p>$/},
|
||||
{input: "Hello world\nIt is a fine day", output: /^<p>Hello world <br \/>\nIt is a fine day<\/p>$/},
|
||||
{input: "\"first\nsecond", output: /^<p>\"first <br \/>\nsecond<\/p>$/},
|
||||
{input: "\'first\nsecond", output: /^<p>\'first <br \/>\nsecond<\/p>$/}
|
||||
],
|
||||
@ -122,7 +123,7 @@ describe("Showdown client side converter", function () {
|
||||
it("should convert newlines in all groups", function () {
|
||||
var testPhrases = [
|
||||
{input: "ruby\npython\nerlang", output: /^<p>ruby <br \/>\npython <br \/>\nerlang<\/p>$/},
|
||||
{input: "Hello world\nIt's a fine day\nout", output: /^<p>Hello world <br \/>\nIt\'s a fine day <br \/>\nout<\/p>$/}
|
||||
{input: "Hello world\nIt is a fine day\nout", output: /^<p>Hello world <br \/>\nIt is a fine day <br \/>\nout<\/p>$/}
|
||||
],
|
||||
processedMarkup;
|
||||
|
||||
@ -134,10 +135,13 @@ describe("Showdown client side converter", function () {
|
||||
|
||||
it("should convert newlines in even long groups", function () {
|
||||
var testPhrases = [
|
||||
{input: "ruby\npython\nerlang\ngo", output: /^<p>ruby <br \/>\npython <br \/>\nerlang <br \/>\ngo<\/p>$/},
|
||||
{
|
||||
input: "Hello world\nIt's a fine day\noutside\nthe window",
|
||||
output: /^<p>Hello world <br \/>\nIt\'s a fine day <br \/>\noutside <br \/>\nthe window<\/p>$/
|
||||
input: "ruby\npython\nerlang\ngo",
|
||||
output: /^<p>ruby <br \/>\npython <br \/>\nerlang <br \/>\ngo<\/p>$/
|
||||
},
|
||||
{
|
||||
input: "Hello world\nIt is a fine day\noutside\nthe window",
|
||||
output: /^<p>Hello world <br \/>\nIt is a fine day <br \/>\noutside <br \/>\nthe window<\/p>$/
|
||||
}
|
||||
],
|
||||
processedMarkup;
|
||||
@ -150,8 +154,14 @@ describe("Showdown client side converter", function () {
|
||||
|
||||
it("should not convert newlines in lists", function () {
|
||||
var testPhrases = [
|
||||
{input: "#fizz\n# buzz\n### baz", output: /^<h1 id="fizz">fizz<\/h1>\n\n<h1 id="buzz">buzz<\/h1>\n\n<h3 id="baz">baz<\/h3>$/},
|
||||
{input: "* foo\n* bar", output: /^<ul>\n<li>foo<\/li>\n<li>bar<\/li>\n<\/ul>$/}
|
||||
{
|
||||
input: "#fizz\n# buzz\n### baz",
|
||||
output: /^<h1 id="fizz">fizz<\/h1>\n\n<h1 id="buzz">buzz<\/h1>\n\n<h3 id="baz">baz<\/h3>$/
|
||||
},
|
||||
{
|
||||
input: "* foo\n* bar",
|
||||
output: /^<ul>\n<li>foo<\/li>\n<li>bar<\/li>\n<\/ul>$/
|
||||
}
|
||||
],
|
||||
processedMarkup;
|
||||
|
||||
@ -212,7 +222,7 @@ describe("Showdown client side converter", function () {
|
||||
var testPhrases = [
|
||||
{
|
||||
input: "[Google][1]\n\n[1]: http://google.co.uk",
|
||||
output: /^<p><a href="http:\/\/google.co.uk">Google<\/a><\/p>$/,
|
||||
output: /^<p><a href="http:\/\/google.co.uk">Google<\/a><\/p>$/
|
||||
},
|
||||
{
|
||||
input: "[Google][1]\n\n[1]: http://google.co.uk \"some text\"",
|
||||
@ -456,4 +466,41 @@ describe("Showdown client side converter", function () {
|
||||
processedMarkup.should.match(testPhrase.output);
|
||||
});
|
||||
});
|
||||
|
||||
it("should output block HTML untouched", function () {
|
||||
var testPhrases = [
|
||||
{
|
||||
input: "<table class=\"test\">\n <tr>\n <td>Foo</td>\n </tr>\n <tr>\n <td>Bar</td>\n </tr>\n</table>",
|
||||
output: /^<table class=\"test\"> \n <tr>\n <td>Foo<\/td>\n <\/tr>\n <tr>\n <td>Bar<\/td>\n <\/tr>\n<\/table>$/
|
||||
},
|
||||
{
|
||||
input: "<hr />",
|
||||
output: /^<hr \/>$/
|
||||
},
|
||||
{ // audio isn't counted as a block tag by showdown so gets wrapped in <p></p>
|
||||
input: "<audio class=\"podcastplayer\" controls>\n <source src=\"foobar.mp3\" type=\"audio/mp3\" preload=\"none\"></source>\n <source src=\"foobar.off\" type=\"audio/ogg\" preload=\"none\"></source>\n</audio>",
|
||||
output: /^<audio class=\"podcastplayer\" controls> \n <source src=\"foobar.mp3\" type=\"audio\/mp3\" preload=\"none\"><\/source>\n <source src=\"foobar.off\" type=\"audio\/ogg\" preload=\"none\"><\/source>\n<\/audio>$/,
|
||||
}
|
||||
];
|
||||
|
||||
testPhrases.forEach(function (testPhrase) {
|
||||
processedMarkup = converter.makeHtml(testPhrase.input);
|
||||
processedMarkup.should.match(testPhrase.output);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
// Waiting for showdown typography to be updated
|
||||
// it("should correctly convert quotes to curly quotes", function () {
|
||||
// var testPhrases = [
|
||||
// {
|
||||
// input: "Hello world\nIt's a fine day\nout",
|
||||
// output: /^<p>Hello world <br \/>\nIt’s a fine day <br \/>\nout<\/p>$/}
|
||||
// ];
|
||||
//
|
||||
// testPhrases.forEach(function (testPhrase) {
|
||||
// processedMarkup = converter.makeHtml(testPhrase.input);
|
||||
// processedMarkup.should.match(testPhrase.output);
|
||||
// });
|
||||
// })
|
||||
});
|
@ -4,12 +4,11 @@
|
||||
*/
|
||||
|
||||
/*globals describe, it */
|
||||
var testUtils = require('../utils'),
|
||||
should = require('should'),
|
||||
var testUtils = require('../utils'),
|
||||
should = require('should'),
|
||||
|
||||
// Stuff we are testing
|
||||
ghPath = "../../shared/lib/showdown/extensions/github.js",
|
||||
github = require(ghPath);
|
||||
ghostgfm = require('../../shared/lib/showdown/extensions/ghostgfm');
|
||||
|
||||
function _ExecuteExtension(ext, text) {
|
||||
if (ext.regex) {
|
||||
@ -21,20 +20,20 @@ function _ExecuteExtension(ext, text) {
|
||||
}
|
||||
|
||||
function _ConvertPhrase(testPhrase) {
|
||||
return github().reduce(function (text, ext) {
|
||||
return ghostgfm().reduce(function (text, ext) {
|
||||
return _ExecuteExtension(ext, text);
|
||||
}, testPhrase);
|
||||
}
|
||||
|
||||
|
||||
describe("Github showdown extensions", function () {
|
||||
describe("Ghost GFM showdown extension", function () {
|
||||
/*jslint regexp: true */
|
||||
|
||||
it("should export an array of methods for processing", function () {
|
||||
github.should.be.a.function;
|
||||
github().should.be.an.Array;
|
||||
ghostgfm.should.be.a.function;
|
||||
ghostgfm().should.be.an.Array;
|
||||
|
||||
github().forEach(function (processor) {
|
||||
ghostgfm().forEach(function (processor) {
|
||||
processor.should.be.an.Object;
|
||||
processor.should.have.property("type");
|
||||
processor.type.should.be.a.String;
|
||||
@ -50,6 +49,14 @@ describe("Github showdown extensions", function () {
|
||||
processedMarkup.should.match(testPhrase.output);
|
||||
});
|
||||
|
||||
it("should allow 4 underscores", function () {
|
||||
var testPhrase = {input: 'Ghost ____', output: /Ghost\s(?:_){4}$/},
|
||||
processedMarkup = _ConvertPhrase(testPhrase.input);
|
||||
|
||||
processedMarkup.should.match(testPhrase.output);
|
||||
});
|
||||
|
||||
|
||||
it("should auto-link URL in text with markdown syntax", function () {
|
||||
var testPhrases = [
|
||||
{
|
@ -6,21 +6,20 @@
|
||||
*/
|
||||
|
||||
/*globals describe, it */
|
||||
var testUtils = require('../utils'),
|
||||
should = require('should'),
|
||||
var testUtils = require('../utils'),
|
||||
should = require('should'),
|
||||
|
||||
// Stuff we are testing
|
||||
gdPath = "../../clientold/assets/lib/showdown/extensions/ghostdown.js",
|
||||
ghostdown = require(gdPath);
|
||||
ghostimagepreview = require('../../shared/lib/showdown/extensions/ghostimagepreview');
|
||||
|
||||
describe("Ghostdown showdown extensions", function () {
|
||||
describe("Ghost Image Preview showdown extension", function () {
|
||||
|
||||
it("should export an array of methods for processing", function () {
|
||||
|
||||
ghostdown.should.be.a.function;
|
||||
ghostdown().should.be.an.instanceof(Array);
|
||||
ghostimagepreview.should.be.a.function;
|
||||
ghostimagepreview().should.be.an.instanceof(Array);
|
||||
|
||||
ghostdown().forEach(function (processor) {
|
||||
ghostimagepreview().forEach(function (processor) {
|
||||
processor.should.be.an.Object;
|
||||
processor.should.have.property("type");
|
||||
processor.should.have.property("filter");
|
||||
@ -46,7 +45,7 @@ describe("Ghostdown showdown extensions", function () {
|
||||
]
|
||||
.forEach(function (imageMarkup) {
|
||||
var processedMarkup =
|
||||
ghostdown().reduce(function (prev, processor) {
|
||||
ghostimagepreview().reduce(function (prev, processor) {
|
||||
return processor.filter(prev);
|
||||
}, imageMarkup);
|
||||
|
||||
@ -55,15 +54,6 @@ describe("Ghostdown showdown extensions", function () {
|
||||
});
|
||||
});
|
||||
|
||||
it("should allow 4 underscores", function () {
|
||||
var processedMarkup =
|
||||
ghostdown().reduce(function (prev, processor) {
|
||||
return processor.filter(prev);
|
||||
}, "Ghost ____");
|
||||
|
||||
processedMarkup.should.match(/Ghost\s(?:_){4}$/);
|
||||
});
|
||||
|
||||
it("should correctly include an image", function () {
|
||||
[
|
||||
"![image and another,/ image](http://dsurl.stuff)",
|
||||
@ -75,7 +65,7 @@ describe("Ghostdown showdown extensions", function () {
|
||||
]
|
||||
.forEach(function (imageMarkup) {
|
||||
var processedMarkup =
|
||||
ghostdown().reduce(function (prev, processor) {
|
||||
ghostimagepreview().reduce(function (prev, processor) {
|
||||
return processor.filter(prev);
|
||||
}, imageMarkup);
|
||||
|
@ -2,6 +2,7 @@
|
||||
var fs = require('fs-extra'),
|
||||
path = require('path'),
|
||||
should = require('should'),
|
||||
config = require('../../server/config'),
|
||||
sinon = require('sinon'),
|
||||
when = require('when'),
|
||||
localfilesystem = require('../../server/storage/localfilesystem');
|
||||
@ -44,7 +45,7 @@ describe('Local File System Storage', function () {
|
||||
it('should send correct path to image when original file has spaces', function (done) {
|
||||
image.name = 'AN IMAGE.jpg';
|
||||
localfilesystem.save(image).then(function (url) {
|
||||
url.should.equal('/content/images/2013/Sep/AN_IMAGE.jpg');
|
||||
url.should.equal('/content/images/2013/Sep/AN-IMAGE.jpg');
|
||||
return done();
|
||||
}).then(null, done);
|
||||
});
|
||||
@ -122,6 +123,27 @@ describe('Local File System Storage', function () {
|
||||
}).then(null, done);
|
||||
});
|
||||
|
||||
describe('when a custom content path is used', function () {
|
||||
var origContentPath = config().paths.contentPath;
|
||||
var origImagesPath = config().paths.imagesPath;
|
||||
|
||||
beforeEach(function () {
|
||||
config().paths.contentPath = config().paths.appRoot + '/var/ghostcms';
|
||||
config().paths.imagesPath = config().paths.appRoot + '/var/ghostcms/' + config().paths.imagesRelPath;
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
config().paths.contentPath = origContentPath;
|
||||
config().paths.imagesPath = origImagesPath;
|
||||
});
|
||||
|
||||
it('should send the correct path to image', function (done) {
|
||||
localfilesystem.save(image).then(function (url) {
|
||||
url.should.equal('/content/images/2013/Sep/IMAGE.jpg');
|
||||
return done();
|
||||
}).then(null, done);
|
||||
});
|
||||
});
|
||||
|
||||
describe('on Windows', function () {
|
||||
var truePathSep = path.sep;
|
||||
@ -139,7 +161,15 @@ describe('Local File System Storage', function () {
|
||||
path.sep = '\\';
|
||||
path.join.returns('content\\images\\2013\\Sep\\IMAGE.jpg');
|
||||
localfilesystem.save(image).then(function (url) {
|
||||
url.should.equal('/content/images/2013/Sep/IMAGE.jpg');
|
||||
if (truePathSep === '\\') {
|
||||
url.should.equal('/content/images/2013/Sep/IMAGE.jpg');
|
||||
} else {
|
||||
// if this unit test is run on an OS that uses forward slash separators,
|
||||
// localfilesystem.save() will use a path.relative() call on
|
||||
// one path with backslash separators and one path with forward
|
||||
// slashes and it returns a path that needs to be normalized
|
||||
path.normalize(url).should.equal('/content/images/2013/Sep/IMAGE.jpg');
|
||||
}
|
||||
return done();
|
||||
}).then(null, done);
|
||||
});
|
||||
|
46
core/test/unit/xmlrpc_spec.js
Normal file
46
core/test/unit/xmlrpc_spec.js
Normal file
@ -0,0 +1,46 @@
|
||||
/*globals describe, beforeEach, afterEach, it*/
|
||||
var assert = require('assert'),
|
||||
http = require('http'),
|
||||
nock = require('nock'),
|
||||
settings = require('../../server/api').settings;
|
||||
should = require('should'),
|
||||
sinon = require('sinon'),
|
||||
testUtils = require('../utils'),
|
||||
when = require('when'),
|
||||
xmlrpc = require('../../server/xmlrpc'),
|
||||
// storing current environment
|
||||
currentEnv = process.env.NODE_ENV;
|
||||
|
||||
describe('XMLRPC', function () {
|
||||
var sandbox;
|
||||
|
||||
beforeEach(function () {
|
||||
sandbox = sinon.sandbox.create();
|
||||
// give environment a value that will ping
|
||||
process.env.NODE_ENV = "production";
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
sandbox.restore();
|
||||
// reset the environment
|
||||
process.env.NODE_ENV = currentEnv;
|
||||
});
|
||||
|
||||
|
||||
it('should execute two pings', function (done) {
|
||||
var ping1 = nock('http://blogsearch.google.com').post('/ping/RPC2').reply(200),
|
||||
ping2 = nock('http://rpc.pingomatic.com').post('/').reply(200),
|
||||
testPost = testUtils.DataGenerator.Content.posts[2],
|
||||
settingsStub = sandbox.stub(settings, 'read', function () {
|
||||
return when({value: '/:slug/'});
|
||||
});
|
||||
|
||||
xmlrpc.ping(testPost).then(function () {
|
||||
ping1.isDone().should.be.true;
|
||||
ping2.isDone().should.be.true;
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
@ -21,6 +21,9 @@ var _ = require('lodash'),
|
||||
notification: ['type', 'message', 'status', 'id']
|
||||
};
|
||||
|
||||
function getApiQuery (route) {
|
||||
return url.resolve(ApiRouteBase, route);
|
||||
}
|
||||
|
||||
function getApiURL (route) {
|
||||
var baseURL = url.resolve(schema + host + ':' + port, ApiRouteBase);
|
||||
@ -46,6 +49,7 @@ function checkResponseValue (jsonResponse, properties) {
|
||||
|
||||
module.exports = {
|
||||
getApiURL: getApiURL,
|
||||
getApiQuery: getApiQuery,
|
||||
getSigninURL: getSigninURL,
|
||||
getAdminURL: getAdminURL,
|
||||
checkResponse: checkResponse,
|
||||
|
@ -1,5 +1,6 @@
|
||||
var knex = require('../../server/models/base').knex,
|
||||
when = require('when'),
|
||||
sequence = require('when/sequence'),
|
||||
nodefn = require('when/node/function'),
|
||||
_ = require('lodash'),
|
||||
fs = require('fs-extra'),
|
||||
@ -29,14 +30,13 @@ function insertPosts() {
|
||||
function insertMorePosts(max) {
|
||||
var lang,
|
||||
status,
|
||||
posts,
|
||||
posts = [],
|
||||
promises = [],
|
||||
i, j, k = 0;
|
||||
|
||||
max = max || 50;
|
||||
|
||||
for (i = 0; i < 2; i += 1) {
|
||||
posts = [];
|
||||
lang = i % 2 ? 'en' : 'fr';
|
||||
posts.push(DataGenerator.forKnex.createGenericPost(k++, null, lang));
|
||||
|
||||
@ -44,18 +44,21 @@ function insertMorePosts(max) {
|
||||
status = j % 2 ? 'draft' : 'published';
|
||||
posts.push(DataGenerator.forKnex.createGenericPost(k++, status, lang));
|
||||
}
|
||||
|
||||
promises.push(knex('posts').insert(posts));
|
||||
}
|
||||
|
||||
return when.all(promises);
|
||||
return sequence(_.times(posts.length, function(index) {
|
||||
return function() {
|
||||
return knex('posts').insert(posts[index]);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
function insertMorePostsTags(max) {
|
||||
max = max || 50;
|
||||
|
||||
return when.all([
|
||||
knex('posts').select('id'),
|
||||
// PostgreSQL can return results in any order
|
||||
knex('posts').orderBy('id', 'asc').select('id'),
|
||||
knex('tags').select('id', 'name')
|
||||
]).then(function (results) {
|
||||
var posts = _.pluck(results[0], 'id'),
|
||||
@ -74,9 +77,11 @@ function insertMorePostsTags(max) {
|
||||
promises.push(DataGenerator.forKnex.createPostsTags(posts[i], injectionTagId));
|
||||
}
|
||||
|
||||
promises.push(knex('posts_tags').insert(promises));
|
||||
|
||||
return when.all(promises);
|
||||
return sequence(_.times(promises.length, function(index) {
|
||||
return function() {
|
||||
return knex('posts_tags').insert(promises[index]);
|
||||
};
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
|
12
package.json
12
package.json
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name" : "ghost",
|
||||
"version" : "0.4.1",
|
||||
"version" : "0.4.2",
|
||||
"description" : "Just a blogging platform.",
|
||||
"author" : "Ghost Foundation",
|
||||
"homepage" : "http://ghost.org",
|
||||
@ -40,7 +40,7 @@
|
||||
"express": "3.4.6",
|
||||
"express-hbs": "0.7.9",
|
||||
"fs-extra": "0.8.1",
|
||||
"knex": "0.5.0",
|
||||
"knex": "0.5.8",
|
||||
"lodash": "2.4.1",
|
||||
"moment": "2.4.0",
|
||||
"node-polyglot": "0.3.0",
|
||||
@ -48,14 +48,15 @@
|
||||
"nodemailer": "0.5.13",
|
||||
"rss": "0.2.1",
|
||||
"semver": "2.2.1",
|
||||
"showdown": "0.3.1",
|
||||
"showdown": "https://github.com/ErisDS/showdown/archive/v0.3.2-ghost.tar.gz",
|
||||
"sqlite3": "2.2.0",
|
||||
"unidecode": "0.1.3",
|
||||
"validator": "3.4.0",
|
||||
"when": "2.7.0"
|
||||
"when": "2.7.0",
|
||||
"xml": "0.0.12"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"mysql": "2.0.0-alpha9"
|
||||
"mysql": "2.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"blanket": "~1.1.5",
|
||||
@ -80,6 +81,7 @@
|
||||
"grunt-update-submodules": "~0.2.1",
|
||||
"matchdep": "~0.3.0",
|
||||
"mocha": "~1.15.1",
|
||||
"nock": "0.27.2",
|
||||
"rewire": "~2.0.0",
|
||||
"request": "~2.29.0",
|
||||
"require-dir": "~0.1.0",
|
||||
|
Loading…
Reference in New Issue
Block a user