32 | Stack Trace
42 |{{message}}
43 |-
44 | {{#foreach stack}}
45 |
- 46 | at 47 | {{#if function}}{{function}}{{/if}} 48 | ({{at}}) 49 | 50 | {{/foreach}} 51 |
├── core ├── client │ ├── tpl │ │ ├── modals │ │ │ ├── blank.hbs │ │ │ ├── copyToHTML.hbs │ │ │ ├── uploadImage.hbs │ │ │ └── markdown.hbs │ │ ├── notification.hbs │ │ ├── settings │ │ │ ├── sidebar.hbs │ │ │ ├── general.hbs │ │ │ └── user-profile.hbs │ │ ├── forgotten.hbs │ │ ├── list-item.hbs │ │ ├── signup.hbs │ │ ├── login.hbs │ │ ├── modal.hbs │ │ └── preview.hbs │ ├── assets │ │ ├── sass │ │ │ ├── ie.scss │ │ │ ├── modules │ │ │ │ └── animations.scss │ │ │ ├── screen.scss │ │ │ └── layouts │ │ │ │ ├── errors.scss │ │ │ │ └── plugins.scss │ │ ├── img │ │ │ ├── large.png │ │ │ ├── medium.png │ │ │ ├── small.png │ │ │ ├── 404-ghost.png │ │ │ ├── loadingcat.gif │ │ │ ├── 404-ghost@2x.png │ │ │ ├── touch-icon-ipad.png │ │ │ └── touch-icon-iphone.png │ │ ├── fonts │ │ │ ├── icons.eot │ │ │ ├── icons.ttf │ │ │ └── icons.woff │ │ └── vendor │ │ │ ├── to-title-case.js │ │ │ ├── showdown │ │ │ └── extensions │ │ │ │ └── ghostdown.js │ │ │ ├── codemirror │ │ │ ├── addon │ │ │ │ └── mode │ │ │ │ │ └── overlay.js │ │ │ └── mode │ │ │ │ └── gfm │ │ │ │ ├── index.html │ │ │ │ ├── gfm.js │ │ │ │ └── test.js │ │ │ └── icheck │ │ │ └── jquery.icheck.min.js │ ├── models │ │ ├── tag.js │ │ ├── themes.js │ │ ├── settings.js │ │ ├── user.js │ │ ├── base.js │ │ ├── uploadModal.js │ │ ├── widget.js │ │ └── post.js │ ├── helpers │ │ └── index.js │ ├── views │ │ ├── debug.js │ │ └── login.js │ ├── init.js │ ├── toggle.js │ ├── mobile-interactions.js │ └── router.js ├── test │ ├── unit │ │ ├── fixtures │ │ │ ├── test.hbs │ │ │ └── theme │ │ │ │ └── partials │ │ │ │ └── test.hbs │ │ ├── api_posts_spec.js │ │ ├── plugins_spec.js │ │ ├── export_spec.js │ │ ├── testUtils.js │ │ ├── client_ghostdown_spec.js │ │ ├── middleware_spec.js │ │ ├── model_roles_spec.js │ │ ├── utils │ │ │ └── api.js │ │ ├── model_permissions_spec.js │ │ ├── errorHandling_spec.js │ │ ├── import_spec.js │ │ ├── model_users_spec.js │ │ └── mail_spec.js │ └── functional │ │ ├── frontend │ │ ├── route_test.js │ │ └── rss_test.js │ │ └── admin │ │ ├── flow_test.js │ │ ├── content_test.js │ │ ├── logout_test.js │ │ └── login_test.js ├── shared │ ├── img │ │ ├── user-cover.png │ │ └── user-image.png │ ├── lang │ │ ├── en_US.json │ │ └── i18n.js │ ├── favicon.ico │ └── vendor │ │ └── showdown │ │ └── extensions │ │ └── github.js ├── server │ ├── views │ │ ├── login.hbs │ │ ├── forgotten.hbs │ │ ├── signup.hbs │ │ ├── settings.hbs │ │ ├── partials │ │ │ ├── notifications.hbs │ │ │ └── navbar.hbs │ │ ├── content.hbs │ │ ├── error.hbs │ │ ├── debug.hbs │ │ ├── user-error.hbs │ │ ├── default.hbs │ │ └── editor.hbs │ ├── helpers │ │ └── tpl │ │ │ ├── nav.hbs │ │ │ └── pagination.hbs │ ├── permissions │ │ └── objectTypeModelMap.js │ ├── data │ │ ├── import │ │ │ ├── index.js │ │ │ └── 000.js │ │ ├── default-settings.json │ │ └── export │ │ │ └── index.js │ ├── models │ │ ├── index.js │ │ ├── role.js │ │ ├── permission.js │ │ ├── tag.js │ │ └── settings.js │ ├── middleware.js │ ├── plugins │ │ ├── GhostPlugin.js │ │ ├── loader.js │ │ └── index.js │ ├── require-tree.js │ ├── mail.js │ └── controllers │ │ └── frontend.js └── config-loader.js ├── content ├── plugins │ └── README.md ├── images │ └── README.md └── data │ └── README.md ├── .gitmodules ├── SECURITY.md ├── .travis.yml ├── index.js ├── .gitignore ├── LICENSE ├── package.json ├── config.example.js └── README.md /core/client/tpl/modals/blank.hbs: -------------------------------------------------------------------------------- 1 | {{content.text}} -------------------------------------------------------------------------------- /core/test/unit/fixtures/test.hbs: -------------------------------------------------------------------------------- 1 |
3 |
4 |
--------------------------------------------------------------------------------
/core/client/tpl/notification.hbs:
--------------------------------------------------------------------------------
1 |
6 | {{message}}
17 |
32 | {{message}}
43 |Optionally depends on other modes for properly highlighted code blocks.
70 | 71 |Parsing/Highlighting Tests: normal, verbose.
72 | 73 | 74 | 75 | -------------------------------------------------------------------------------- /core/test/functional/admin/flow_test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Tests the flow of creating, editing and publishing tests 3 | */ 4 | 5 | /*globals casper, __utils__, url, testPost */ 6 | CasperTest.begin("Ghost edit draft flow works correctly", 8, function suite(test) { 7 | casper.thenOpen(url + "ghost/editor/", function then() { 8 | test.assertUrlMatch(/ghost\/editor\/$/, "Ghost doesn't require login this time"); 9 | }); 10 | 11 | casper.then(function createTestPost() { 12 | casper.sendKeys('#entry-title', testPost.title); 13 | casper.writeContentToCodeMirror(testPost.html); 14 | }); 15 | 16 | casper.waitForSelectorTextChange('.entry-preview .rendered-markdown', function onSuccess() { 17 | test.assertSelectorHasText('.entry-preview .rendered-markdown', 'test', 'Editor value is correct'); 18 | }); 19 | 20 | casper.thenClick('.js-publish-button'); 21 | casper.waitForResource(/posts/); 22 | 23 | casper.waitForSelector('.notification-success', function onSuccess() { 24 | test.assert(true, 'Got success notification'); 25 | }, function onTimeout() { 26 | test.assert(false, 'No success notification :('); 27 | }); 28 | 29 | casper.thenOpen(url + 'ghost/content/', function then() { 30 | test.assertUrlMatch(/ghost\/content\//, "Ghost successfully loaded the content page"); 31 | }); 32 | 33 | casper.then(function then() { 34 | test.assertEvalEquals(function () { 35 | return document.querySelector('.content-list-content li').className; 36 | }, "active", "first item is active"); 37 | 38 | test.assertSelectorHasText(".content-list-content li:first-child h3", testPost.title, "first item is the post we created"); 39 | }); 40 | 41 | casper.thenClick('.post-edit').waitForResource(/editor/, function then() { 42 | test.assertUrlMatch(/editor/, "Ghost sucessfully loaded the editor page again"); 43 | }); 44 | 45 | casper.thenClick('.js-publish-button'); 46 | casper.waitForResource(/posts/); 47 | 48 | casper.waitForSelector('.notification-success', function onSuccess() { 49 | test.assert(true, 'Got success notification'); 50 | }, function onTimeout() { 51 | test.assert(false, 'No success notification :('); 52 | }); 53 | }); 54 | 55 | // TODO: test publishing, editing, republishing, unpublishing etc 56 | //CasperTest.begin("Ghost edit published flow works correctly", 6, function suite(test) { 57 | // 58 | // 59 | // 60 | //}); -------------------------------------------------------------------------------- /core/test/unit/testUtils.js: -------------------------------------------------------------------------------- 1 | var knex = require('../../server/models/base').Knex, 2 | when = require('when'), 3 | migration = require("../../server/data/migration/"), 4 | Settings = require('../../server/models/settings').Settings, 5 | DataGenerator = require('./fixtures/data-generator'), 6 | API = require('./utils/api'); 7 | 8 | function initData() { 9 | return migration.init(); 10 | } 11 | 12 | function clearData() { 13 | // we must always try to delete all tables 14 | return migration.reset(); 15 | } 16 | 17 | function insertDefaultFixtures() { 18 | var promises = []; 19 | 20 | promises.push(insertDefaultUser()); 21 | promises.push(insertPosts()); 22 | 23 | return when.all(promises); 24 | } 25 | 26 | function insertPosts() { 27 | var promises = []; 28 | 29 | promises.push(knex('posts').insert(DataGenerator.forKnex.posts)); 30 | promises.push(knex('tags').insert(DataGenerator.forKnex.tags)); 31 | promises.push(knex('posts_tags').insert(DataGenerator.forKnex.posts_tags)); 32 | 33 | return when.all(promises); 34 | } 35 | 36 | function insertMorePosts() { 37 | var lang, 38 | status, 39 | posts, 40 | promises = [], 41 | i, j, k = 0; 42 | 43 | for (i = 0; i < 2; i += 1) { 44 | posts = []; 45 | lang = i % 2 ? 'en' : 'fr'; 46 | posts.push(DataGenerator.forKnex.createGenericPost(k++, null, lang)); 47 | 48 | for (j = 0; j < 50; j += 1) { 49 | status = j % 2 ? 'published' : 'draft'; 50 | posts.push(DataGenerator.forKnex.createGenericPost(k++, status, lang)); 51 | } 52 | 53 | promises.push(knex('posts').insert(posts)); 54 | } 55 | 56 | return when.all(promises); 57 | } 58 | 59 | function insertDefaultUser() { 60 | var users = [], 61 | userRoles = [], 62 | u_promises = []; 63 | 64 | users.push(DataGenerator.forKnex.createUser(DataGenerator.Content.users[0])); 65 | u_promises.push(knex('users').insert(users)); 66 | userRoles.push(DataGenerator.forKnex.createUserRole(1, 1)); 67 | u_promises.push(knex('roles_users').insert(userRoles)); 68 | 69 | return when.all(u_promises); 70 | } 71 | 72 | module.exports = { 73 | initData: initData, 74 | clearData: clearData, 75 | insertDefaultFixtures: insertDefaultFixtures, 76 | insertPosts: insertPosts, 77 | insertMorePosts: insertMorePosts, 78 | insertDefaultUser: insertDefaultUser, 79 | 80 | DataGenerator: DataGenerator, 81 | API: API 82 | }; 83 | -------------------------------------------------------------------------------- /core/server/plugins/loader.js: -------------------------------------------------------------------------------- 1 | 2 | var path = require('path'), 3 | _ = require('underscore'), 4 | when = require('when'), 5 | ghostInstance, 6 | loader; 7 | 8 | function getGhostInstance() { 9 | if (ghostInstance) { 10 | return ghostInstance; 11 | } 12 | 13 | var Ghost = require('../../ghost'); 14 | 15 | ghostInstance = new Ghost(); 16 | 17 | return ghostInstance; 18 | } 19 | 20 | // Get a relative path to the given plugins root, defaults 21 | // to be relative to __dirname 22 | function getPluginRelativePath(name, relativeTo, ghost) { 23 | ghost = ghost || getGhostInstance(); 24 | relativeTo = relativeTo || __dirname; 25 | 26 | return path.relative(relativeTo, path.join(ghost.paths().pluginPath, name)); 27 | } 28 | 29 | 30 | function getPluginByName(name, ghost) { 31 | ghost = ghost || getGhostInstance(); 32 | 33 | // Grab the plugin class to instantiate 34 | var PluginClass = require(getPluginRelativePath(name)), 35 | plugin; 36 | 37 | // Check for an actual class, otherwise just use whatever was returned 38 | if (_.isFunction(PluginClass)) { 39 | plugin = new PluginClass(ghost); 40 | } else { 41 | plugin = PluginClass; 42 | } 43 | 44 | return plugin; 45 | } 46 | 47 | // The loader is responsible for loading plugins 48 | loader = { 49 | // Load a plugin and return the instantiated plugin 50 | installPluginByName: function (name, ghost) { 51 | var plugin = getPluginByName(name, ghost); 52 | 53 | // Check for an install() method on the plugin. 54 | if (!_.isFunction(plugin.install)) { 55 | return when.reject(new Error("Error loading plugin named " + name + "; no install() method defined.")); 56 | } 57 | 58 | // Wrapping the install() with a when because it's possible 59 | // to not return a promise from it. 60 | return when(plugin.install(ghost)).then(function () { 61 | return when.resolve(plugin); 62 | }); 63 | }, 64 | 65 | // Activate a plugin and return it 66 | activatePluginByName: function (name, ghost) { 67 | var plugin = getPluginByName(name, ghost); 68 | 69 | // Check for an activate() method on the plugin. 70 | if (!_.isFunction(plugin.activate)) { 71 | return when.reject(new Error("Error loading plugin named " + name + "; no activate() method defined.")); 72 | } 73 | 74 | // Wrapping the activate() with a when because it's possible 75 | // to not return a promise from it. 76 | return when(plugin.activate(ghost)).then(function () { 77 | return when.resolve(plugin); 78 | }); 79 | } 80 | }; 81 | 82 | module.exports = loader; -------------------------------------------------------------------------------- /core/client/mobile-interactions.js: -------------------------------------------------------------------------------- 1 | // # Ghost Mobile Interactions 2 | 3 | /*global window, document, $, FastClick */ 4 | 5 | (function () { 6 | 'use strict'; 7 | 8 | FastClick.attach(document.body); 9 | 10 | // ### Show content preview when swiping left on content list 11 | $('.manage').on('click', '.content-list ol li', function (event) { 12 | if (window.matchMedia('(max-width: 800px)').matches) { 13 | event.preventDefault(); 14 | event.stopPropagation(); 15 | $('.content-list').animate({right: '100%', left: '-100%', 'margin-right': '15px'}, 300); 16 | $('.content-preview').animate({right: '0', left: '0', 'margin-left': '0'}, 300); 17 | } 18 | }); 19 | 20 | // ### Hide content preview 21 | $('.manage').on('click', '.content-preview .button-back', function (event) { 22 | if (window.matchMedia('(max-width: 800px)').matches) { 23 | event.preventDefault(); 24 | event.stopPropagation(); 25 | $('.content-list').animate({right: '0', left: '0', 'margin-right': '0'}, 300); 26 | $('.content-preview').animate({right: '-100%', left: '100%', 'margin-left': '15px'}, 300); 27 | } 28 | }); 29 | 30 | // ### Show settings options page when swiping left on settings menu link 31 | $('.settings').on('click', '.settings-menu li', function (event) { 32 | if (window.matchMedia('(max-width: 800px)').matches) { 33 | event.preventDefault(); 34 | event.stopPropagation(); 35 | $('.settings-sidebar').animate({right: '100%', left: '-102%', 'margin-right': '15px'}, 300); 36 | $('.settings-content').animate({right: '0', left: '0', 'margin-left': '0'}, 300); 37 | $('.settings-content .button-back, .settings-content .button-save').css('display', 'inline-block'); 38 | } 39 | }); 40 | 41 | // ### Hide settings options page 42 | $('.settings').on('click', '.settings-content .button-back', function (event) { 43 | if (window.matchMedia('(max-width: 800px)').matches) { 44 | event.preventDefault(); 45 | event.stopPropagation(); 46 | $('.settings-sidebar').animate({right: '0', left: '0', 'margin-right': '0'}, 300); 47 | $('.settings-content').animate({right: '-100%', left: '100%', 'margin-left': '15'}, 300); 48 | $('.settings-content .button-back, .settings-content .button-save').css('display', 'none'); 49 | } 50 | }); 51 | 52 | // ### Toggle the sidebar menu 53 | $('[data-off-canvas]').on('click', function (event) { 54 | if (window.matchMedia('(max-width: 650px)').matches) { 55 | event.preventDefault(); 56 | $('body').toggleClass('off-canvas'); 57 | } 58 | }); 59 | 60 | }()); -------------------------------------------------------------------------------- /core/client/router.js: -------------------------------------------------------------------------------- 1 | /*global window, document, Ghost, Backbone, $, _, NProgress */ 2 | (function () { 3 | "use strict"; 4 | 5 | Ghost.Router = Backbone.Router.extend({ 6 | 7 | routes: { 8 | '' : 'blog', 9 | 'content/' : 'blog', 10 | 'settings(/:pane)/' : 'settings', 11 | 'editor(/:id)/' : 'editor', 12 | 'debug/' : 'debug', 13 | 'register/' : 'register', 14 | 'signup/' : 'signup', 15 | 'signin/' : 'login', 16 | 'forgotten/' : 'forgotten' 17 | }, 18 | 19 | signup: function () { 20 | Ghost.currentView = new Ghost.Views.Signup({ el: '.js-signup-box' }); 21 | }, 22 | 23 | login: function () { 24 | Ghost.currentView = new Ghost.Views.Login({ el: '.js-login-box' }); 25 | }, 26 | 27 | forgotten: function () { 28 | Ghost.currentView = new Ghost.Views.Forgotten({ el: '.js-forgotten-box' }); 29 | }, 30 | 31 | blog: function () { 32 | var posts = new Ghost.Collections.Posts(); 33 | NProgress.start(); 34 | posts.fetch({ data: { status: 'all', orderBy: ['updated_at', 'DESC'] } }).then(function () { 35 | Ghost.currentView = new Ghost.Views.Blog({ el: '#main', collection: posts }); 36 | NProgress.done(); 37 | }); 38 | }, 39 | 40 | settings: function (pane) { 41 | if (!pane) { 42 | // Redirect to settings/general if no pane supplied 43 | this.navigate('/settings/general/', { 44 | trigger: true, 45 | replace: true 46 | }); 47 | return; 48 | } 49 | 50 | // only update the currentView if we don't already have a Settings view 51 | if (!Ghost.currentView || !(Ghost.currentView instanceof Ghost.Views.Settings)) { 52 | Ghost.currentView = new Ghost.Views.Settings({ el: '#main', pane: pane }); 53 | } 54 | }, 55 | 56 | editor: function (id) { 57 | var post = new Ghost.Models.Post(); 58 | post.urlRoot = Ghost.settings.apiRoot + '/posts'; 59 | if (id) { 60 | post.id = id; 61 | post.fetch().then(function () { 62 | Ghost.currentView = new Ghost.Views.Editor({ el: '#main', model: post }); 63 | }); 64 | } else { 65 | Ghost.currentView = new Ghost.Views.Editor({ el: '#main', model: post }); 66 | } 67 | }, 68 | 69 | debug: function () { 70 | Ghost.currentView = new Ghost.Views.Debug({ el: "#main" }); 71 | } 72 | }); 73 | }()); 74 | -------------------------------------------------------------------------------- /core/test/unit/client_ghostdown_spec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Test the ghostdown extension 3 | * 4 | * Only ever runs on the client (i.e in the editor) 5 | * Server processes showdown without it so there can never be an image upload form in a post. 6 | */ 7 | 8 | /*globals describe, it */ 9 | var testUtils = require('./testUtils'), 10 | should = require('should'), 11 | 12 | // Stuff we are testing 13 | gdPath = "../../client/assets/vendor/showdown/extensions/ghostdown.js", 14 | ghostdown = require(gdPath); 15 | 16 | describe("Ghostdown showdown extensions", function () { 17 | 18 | it("should export an array of methods for processing", function () { 19 | 20 | ghostdown.should.be.a("function"); 21 | ghostdown().should.be.an.instanceof(Array); 22 | 23 | ghostdown().forEach(function (processor) { 24 | processor.should.be.a("object"); 25 | processor.should.have.property("type"); 26 | processor.should.have.property("filter"); 27 | processor.type.should.be.a("string"); 28 | processor.filter.should.be.a("function"); 29 | }); 30 | }); 31 | 32 | it("should accurately detect images in markdown", function () { 33 | [ 34 | "![]", 35 | "![]()", 36 | "![image and another,/ image]", 37 | "![image and another,/ image]()", 38 | "", 39 | "" 40 | /* No ref-style for now 41 | "![][]", 42 | "![image and another,/ image][stuff]", 43 | "![][stuff]", 44 | "![image and another,/ image][]" 45 | */ 46 | ] 47 | .forEach(function (imageMarkup) { 48 | var processedMarkup = 49 | ghostdown().reduce(function (prev, processor) { 50 | return processor.filter(prev); 51 | }, imageMarkup); 52 | 53 | // The image is the entire markup, so the image box should be too 54 | processedMarkup.should.match(/^
13 | Change Cover
14 | | Result | 6 |Markdown | 7 |Shortcut | 8 |
|---|---|---|
| Bold | 13 |**text** | 14 |Ctrl / Cmd + B | 15 |
| Emphasize | 18 |_text_ | 19 |Ctrl / Cmd + I | 20 |
Inline Code |
23 | `code` | 24 |Cmd + K / Ctrl + Shift + K | 25 |
| Strike-through | 28 |~~text~~ | 29 |Ctrl + Alt + U | 30 |
| Link | 33 |[title](http://) | 34 |Ctrl + Shift + L | 35 |
| Image | 38 | | 39 |Ctrl + Shift + I | 40 |
| List | 43 |* item | 44 |Ctrl + L | 45 |
| Blockquote | 48 |> quote | 49 |Ctrl + Q | 50 |
| H1 | 53 |# Heading | 54 |Ctrl + Alt + 1 | 55 |
| H2 | 58 |## Heading | 59 |Ctrl + Alt + 2 | 60 |
| H3 | 63 |### Heading | 64 |Ctrl + Alt + 3 | 65 |
| H4 | 68 |#### Heading | 69 |Ctrl + Alt + 4 | 70 |
| H5 | 73 |##### Heading | 74 |Ctrl + Alt + 5 | 75 |
| H6 | 78 |###### Heading | 79 |Ctrl + Alt + 6 | 80 |
| Select Word | 83 |84 | | Ctrl + Alt + W | 85 |
| New Paragraph | 88 |89 | | Ctrl / Cmd + Enter | 90 |
| Uppercase | 93 |94 | | Ctrl + U | 95 |
| Lowercase | 98 |99 | | Ctrl + Shift + U | 100 |
| Titlecase | 103 |104 | | Ctrl + Alt + Shift + U | 105 |
| Insert Current Date | 108 |109 | | Ctrl + Shift + 1 | 110 |
[\s\S]*?<\/pre>/gim, function (x) {
32 | var hash = hashId();
33 | preExtractions[hash] = x;
34 | return "{gfm-js-extract-pre-" + hash + "}";
35 | }, 'm');
36 |
37 | //prevent foo_bar and foo_bar_baz from ending up with an italic word in the middle
38 | text = text.replace(/(^(?! {4}|\t)\w+_\w+_\w[\w_]*)/gm, function (x) {
39 | return x.replace(/_/gm, '\\_');
40 | });
41 |
42 | // in very clear cases, let newlines become
tags
43 | text = text.replace(/^[\w\<][^\n]*\n+/gm, function (x) {
44 | return x.match(/\n{2}/) ? x : x.trim() + " \n";
45 | });
46 |
47 | // better URL support, but no title support
48 | text = text.replace(imageMarkdownRegex, function (match, key, alt, src) {
49 | if (src) {
50 | return '
';
51 | }
52 |
53 | return '';
54 | });
55 |
56 | text = text.replace(/\{gfm-js-extract-pre-([0-9]+)\}/gm, function (x, y) {
57 | return "\n\n" + preExtractions[y];
58 | });
59 |
60 |
61 | return text;
62 | }
63 | },
64 | {
65 | // GFM autolinking & custom image handling, happens AFTER showdown
66 | type : 'html',
67 | filter : function (text) {
68 | var refExtractions = {},
69 | preExtractions = {},
70 | hashID = 0;
71 |
72 | function hashId() {
73 | return hashID++;
74 | }
75 |
76 | // Extract pre blocks
77 | text = text.replace(/<(pre|code)>[\s\S]*?<\/(\1)>/gim, function (x) {
78 | var hash = hashId();
79 | preExtractions[hash] = x;
80 | return "{gfm-js-extract-pre-" + hash + "}";
81 | }, 'm');
82 |
83 | // filter out def urls
84 | // from Marked https://github.com/chjj/marked/blob/master/lib/marked.js#L24
85 | text = text.replace(/^ *\[([^\]]+)\]: *([^\s>]+)>?(?: +["(]([^\n]+)[")])? *(?:\n+|$)/gmi,
86 | function (x) {
87 | var hash = hashId();
88 | refExtractions[hash] = x;
89 | return "{gfm-js-extract-ref-url-" + hash + "}";
90 | });
91 |
92 | // match a URL
93 | // adapted from https://gist.github.com/jorilallo/1283095#L158
94 | // and http://blog.stevenlevithan.com/archives/mimic-lookbehind-javascript
95 | text = text.replace(/(\]\(|\]|\[|]*?\>)?https?\:\/\/[^"\s\<\>]*[^.,;'">\:\s\<\>\)\]\!]/gmi,
96 | function (wholeMatch, lookBehind, matchIndex) {
97 | // Check we are not inside an HTML tag
98 | var left = text.slice(0, matchIndex), right = text.slice(matchIndex);
99 | if ((left.match(/<[^>]+$/) && right.match(/^[^>]*>/)) || lookBehind) {
100 | return wholeMatch;
101 | }
102 | // If we have a matching lookBehind, this is a failure, else wrap the match in tag
103 | return lookBehind ? wholeMatch : "" + wholeMatch + "";
104 | });
105 |
106 | // match email
107 | text = text.replace(/[a-z0-9_\-+=.]+@[a-z0-9\-]+(\.[a-z0-9-]+)+/gmi, function (wholeMatch) {
108 | return "" + wholeMatch + "";
109 | });
110 |
111 | // replace extractions
112 | text = text.replace(/\{gfm-js-extract-pre-([0-9]+)\}/gm, function (x, y) {
113 | return preExtractions[y];
114 | });
115 |
116 | text = text.replace(/\{gfm-js-extract-ref-url-([0-9]+)\}/gi, function (x, y) {
117 | return "\n\n" + refExtractions[y];
118 | });
119 |
120 | return text;
121 | }
122 | }
123 | ];
124 | };
125 |
126 | // Client-side export
127 | if (typeof window !== 'undefined' && window.Showdown && window.Showdown.extensions) { window.Showdown.extensions.github = github; }
128 | // Server-side export
129 | if (typeof module !== 'undefined') module.exports = github;
130 | }());
--------------------------------------------------------------------------------
/core/server/controllers/frontend.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Main controller for Ghost frontend
3 | */
4 |
5 | /*global require, module */
6 |
7 | var Ghost = require('../../ghost'),
8 | api = require('../api'),
9 | RSS = require('rss'),
10 | _ = require('underscore'),
11 | errors = require('../errorHandling'),
12 | when = require('when'),
13 | url = require('url'),
14 |
15 |
16 | ghost = new Ghost(),
17 | frontendControllers;
18 |
19 | frontendControllers = {
20 | 'homepage': function (req, res, next) {
21 | // Parse the page number
22 | var pageParam = req.params.page !== undefined ? parseInt(req.params.page, 10) : 1,
23 | postsPerPage = parseInt(ghost.settings('postsPerPage'), 10),
24 | options = {};
25 |
26 | // No negative pages
27 | if (isNaN(pageParam) || pageParam < 1) {
28 | //redirect to 404 page?
29 | return res.redirect('/');
30 | }
31 | options.page = pageParam;
32 |
33 | // Redirect '/page/1/' to '/' for all teh good SEO
34 | if (pageParam === 1 && req.route.path === '/page/:page/') {
35 | return res.redirect('/');
36 | }
37 |
38 | // No negative posts per page, must be number
39 | if (!isNaN(postsPerPage) && postsPerPage > 0) {
40 | options.limit = postsPerPage;
41 | }
42 |
43 | api.posts.browse(options).then(function (page) {
44 |
45 | var maxPage = page.pages;
46 |
47 | // A bit of a hack for situations with no content.
48 | if (maxPage === 0) {
49 | maxPage = 1;
50 | page.pages = 1;
51 | }
52 |
53 | // If page is greater than number of pages we have, redirect to last page
54 | if (pageParam > maxPage) {
55 | return res.redirect(maxPage === 1 ? '/' : ('/page/' + maxPage + '/'));
56 | }
57 |
58 | // Render the page of posts
59 | ghost.doFilter('prePostsRender', page.posts, function (posts) {
60 | res.render('index', {posts: posts, pagination: {page: page.page, prev: page.prev, next: page.next, limit: page.limit, total: page.total, pages: page.pages}});
61 | });
62 | }).otherwise(function (err) {
63 | return next(new Error(err));
64 | });
65 | },
66 | 'single': function (req, res, next) {
67 | api.posts.read({'slug': req.params.slug}).then(function (post) {
68 | if (post) {
69 | ghost.doFilter('prePostsRender', post.toJSON(), function (post) {
70 | res.render('post', {post: post});
71 | });
72 | } else {
73 | next();
74 | }
75 |
76 | }).otherwise(function (err) {
77 | return next(new Error(err));
78 | });
79 | },
80 | 'rss': function (req, res, next) {
81 | // Initialize RSS
82 | var siteUrl = ghost.config().url,
83 | pageParam = req.params.page !== undefined ? parseInt(req.params.page, 10) : 1,
84 | feed;
85 | //needs refact for multi user to not use first user as default
86 | api.users.read({id : 1}).then(function (user) {
87 | feed = new RSS({
88 | title: ghost.settings('title'),
89 | description: ghost.settings('description'),
90 | generator: 'Ghost v' + res.locals.version,
91 | author: user ? user.attributes.name : null,
92 | feed_url: url.resolve(siteUrl, '/rss/'),
93 | site_url: siteUrl,
94 | ttl: '60'
95 | });
96 |
97 | // No negative pages
98 | if (isNaN(pageParam) || pageParam < 1) {
99 | return res.redirect('/rss/');
100 | }
101 |
102 | if (pageParam === 1 && req.route.path === '/rss/:page/') {
103 | return res.redirect('/rss/');
104 | }
105 |
106 | api.posts.browse({page: pageParam}).then(function (page) {
107 | var maxPage = page.pages;
108 |
109 | // A bit of a hack for situations with no content.
110 | if (maxPage === 0) {
111 | maxPage = 1;
112 | page.pages = 1;
113 | }
114 |
115 | // If page is greater than number of pages we have, redirect to last page
116 | if (pageParam > maxPage) {
117 | return res.redirect('/rss/' + maxPage + '/');
118 | }
119 |
120 | ghost.doFilter('prePostsRender', page.posts, function (posts) {
121 | posts.forEach(function (post) {
122 | var item = {
123 | title: _.escape(post.title),
124 | guid: post.uuid,
125 | url: siteUrl + '/' + post.slug + '/',
126 | date: post.published_at,
127 | },
128 | content = post.html;
129 |
130 | //set img src to absolute url
131 | content = content.replace(/src=["|'|\s]?([\w\/\?\$\.\+\-;%:@&=,_]+)["|'|\s]?/gi, function (match, p1) {
132 | p1 = url.resolve(siteUrl, p1);
133 | return "src='" + p1 + "' ";
134 | });
135 | //set a href to absolute url
136 | content = content.replace(/href=["|'|\s]?([\w\/\?\$\.\+\-;%:@&=,_]+)["|'|\s]?/gi, function (match, p1) {
137 | p1 = url.resolve(siteUrl, p1);
138 | return "href='" + p1 + "' ";
139 | });
140 | item.description = content;
141 | feed.item(item);
142 | });
143 | res.set('Content-Type', 'text/xml');
144 | res.send(feed.xml());
145 | });
146 | });
147 | }).otherwise(function (err) {
148 | return next(new Error(err));
149 | });
150 | }
151 |
152 | };
153 |
154 | module.exports = frontendControllers;
--------------------------------------------------------------------------------