From 4d5bc8708eac8e782f53fc9d934ef25f94f96add Mon Sep 17 00:00:00 2001 From: buddha87 Date: Wed, 4 Jan 2017 14:53:25 +0100 Subject: [PATCH] Initial Notification Rewrite + Markdown posts (experimental). --- composer.json | 3 +- js/humhub/humhub.client.js | 7 +- js/humhub/humhub.log.js | 5 +- js/humhub/humhub.ui.additions.js | 62 +- js/humhub/humhub.ui.richtext.js | 33 +- js/humhub/legacy/jquery.flatelements.js | 4 +- less/markdown.less | 375 ++- protected/config/web.php | 2 +- protected/humhub/assets/AppAsset.php | 1 + .../humhub/assets/PagedownConverterAsset.php | 40 + protected/humhub/components/Module.php | 5 + .../humhub/components/SettingsManager.php | 18 +- .../humhub/components/SocialActivity.php | 253 +- .../components/rendering/LayoutRenderer.php | 59 + .../rendering/MailLayoutRenderer.php | 42 + .../humhub/components/rendering/Renderer.php | 25 + .../components/rendering/ViewPathRenderer.php | 77 + .../humhub/components/rendering/Viewable.php | 39 + .../validators/AbstractDateValidator.php | 7 - .../validators/PastDateValidator.php | 7 - protected/humhub/config/common.php | 14 + protected/humhub/docs/CHANGELOG.md | 3 +- protected/humhub/libs/Viewable.php | 158 -- .../components/ActivityMailRenderer.php | 23 + .../activity/components/BaseActivity.php | 86 +- .../m161228_131023_rename_source_fields.php | 30 + .../modules/activity/models/Activity.php | 6 +- protected/humhub/modules/admin/Module.php | 2 +- .../admin/controllers/SettingController.php | 16 +- .../admin/models/forms/UserEditForm.php | 7 - .../AdminNotificationCategory.php | 28 + .../notifications/NewVersionAvailable.php | 10 +- .../admin/views/setting/notification.php | 22 + .../modules/admin/widgets/SettingsMenu.php | 26 +- .../comment/notifications/NewComment.php | 8 - .../codeception/fixtures/data/comment.php | 7 - .../content/activities/ContentCreated.php | 4 +- .../components/ContentActiveRecord.php | 5 +- .../components/ContentAddonActiveRecord.php | 3 +- .../ContentContainerSettingsManager.php | 17 + .../content/interfaces/ContentOwner.php | 15 + .../interfaces/ContentTitlePreview.php | 21 - .../content/notifications/ContentCreated.php | 2 +- .../fixtures/ContentContainerFixture.php | 3 +- .../ContentContainerSettingFixture.php | 19 + .../data/contentcontainer_setting.php | 20 + .../FriendshipNotificationCategory.php | 52 + .../friendship/notifications/Request.php | 8 +- .../notifications/RequestApproved.php | 22 +- .../notifications/RequestDeclined.php | 5 + .../modules/like/notifications/NewLike.php | 19 +- .../tests/codeception/fixtures/data/like.php | 6 - .../components/BaseNotification.php | 307 ++- .../components/MailNotificationTarget.php | 76 + .../components/MailTargetRenderer.php | 125 + .../components/MobileNotificationTarget.php | 33 + .../components/NotificationCategory.php | 102 + .../components/NotificationManager.php | 133 + .../components/NotificationTarget.php | 242 ++ .../components/WebNotificationTarget.php | 32 + .../components/WebTargetRenderer.php | 95 + .../notification/models/Notification.php | 22 +- .../models/forms/NotificationSettings.php | 53 + .../codeception/_support/AcceptanceTester.php | 2 +- .../codeception/_support/FunctionalTester.php | 2 +- .../tests/codeception/_support/UnitTester.php | 2 +- .../_generated/AcceptanceTesterActions.php | 2 +- .../_generated/FunctionalTesterActions.php | 2 +- .../_support/_generated/UnitTesterActions.php | 2 +- .../fixtures/data/notification.php | 7 - .../category/NotificationCategoryTest.php | 154 ++ .../notifications/SpecialNotification.php | 20 + .../SpecialNotificationCategory.php | 49 + .../notifications/TestNotification.php | 19 + .../TestNotificationCategory.php | 40 + .../unit/rendering/MailTargetRenderTest.php | 43 + .../unit/rendering/WebTargetRenderTest.php | 46 + .../TestedMailViewNotification.php | 21 + .../mail/plaintext/specialLayout.php | 2 + .../notification/mail/specialLayout.php | 2 + .../layouts/notification/specialLayout.php | 2 + .../views/notification/mail/special.php | 2 + .../rendering/views/notification/special.php | 2 + .../notification/views/layouts/mail.php | 2 +- .../notification/views/layouts/web.php | 6 +- .../views/notification/default.php | 2 +- .../views/notification/mail/default.php | 1 + .../notification/mail/plaintext/default.php | 2 +- .../widgets/NotificationSettingsForm.php | 38 + .../views/notificationSettingsForm.php | 43 + .../modules/post/resources/js/humhub.post.js | 1 + .../modules/post/widgets/views/wallEntry.php | 8 +- .../space/behaviors/SpaceModelMembership.php | 97 +- .../controllers/MembershipController.php | 2 +- .../modules/space/models/Membership.php | 3 - .../space/notifications/ApprovalRequest.php | 10 +- .../notifications/ApprovalRequestAccepted.php | 10 +- .../notifications/ApprovalRequestDeclined.php | 10 +- .../modules/space/notifications/Invite.php | 10 +- .../space/notifications/InviteAccepted.php | 10 +- .../space/notifications/InviteDeclined.php | 10 +- .../SpaceMemberNotificationCategory.php | 52 + .../modules/space/widgets/InviteModal.php | 7 - .../modules/stream/assets/js/humhub.stream.js | 2 +- .../codeception/acceptance/StreamCest.php | 20 +- .../humhub/modules/user/models/Follow.php | 24 +- .../user/models/forms/EditGroupForm.php | 7 - .../modules/user/notifications/Followed.php | 4 +- .../modules/user/notifications/Mentioned.php | 17 +- .../codeception/_support/AcceptanceTester.php | 2 +- .../codeception/fixtures/data/setting.php | 44 +- protected/humhub/tests/config/test.php | 7 + protected/humhub/widgets/JsWidget.php | 7 - protected/humhub/widgets/ModalDialog.php | 7 - protected/humhub/widgets/RichText.php | 6 +- resources/js/pagedown/.hg/00changelog.i | Bin 0 -> 57 bytes resources/js/pagedown/.hg/branch | 1 + resources/js/pagedown/.hg/cache/branchheads | 2 + resources/js/pagedown/.hg/cache/tags | 2 + resources/js/pagedown/.hg/dirstate | Bin 0 -> 496 bytes resources/js/pagedown/.hg/hgrc | 2 + resources/js/pagedown/.hg/requires | 4 + resources/js/pagedown/.hg/store/00changelog.i | Bin 0 -> 14624 bytes resources/js/pagedown/.hg/store/00manifest.i | Bin 0 -> 12846 bytes .../.hg/store/data/_l_i_c_e_n_s_e.txt.i | Bin 0 -> 1181 bytes .../.hg/store/data/_markdown._converter.js.i | Bin 0 -> 27633 bytes .../.hg/store/data/_markdown._editor.js.i | Bin 0 -> 42637 bytes .../.hg/store/data/_markdown._sanitizer.js.i | Bin 0 -> 2201 bytes .../.hg/store/data/_markdown.local.en.js.i | Bin 0 -> 608 bytes .../.hg/store/data/_markdown.local.fr.js.i | Bin 0 -> 650 bytes .../.hg/store/data/_r_e_a_d_m_e.txt.i | Bin 0 -> 64 bytes .../.hg/store/data/demo/browser/demo.css.i | Bin 0 -> 823 bytes .../.hg/store/data/demo/browser/demo.html.i | Bin 0 -> 1594 bytes .../.hg/store/data/demo/node/demo.js.i | Bin 0 -> 787 bytes .../store/data/local/_markdown.local.fr.js.i | Bin 0 -> 1706 bytes .../.hg/store/data/node-pagedown.js.i | Bin 0 -> 145 bytes .../js/pagedown/.hg/store/data/package.json.i | Bin 0 -> 413 bytes .../store/data/resources/wmd-buttons.psd.i | Bin 0 -> 20633 bytes .../pagedown/.hg/store/data/wmd-buttons.png.i | Bin 0 -> 7530 bytes resources/js/pagedown/.hg/store/fncache | 15 + resources/js/pagedown/.hg/store/phaseroots | 0 resources/js/pagedown/.hg/store/undo | Bin 0 -> 456 bytes .../js/pagedown/.hg/store/undo.phaseroots | 0 resources/js/pagedown/.hg/undo.bookmarks | 0 resources/js/pagedown/.hg/undo.branch | 1 + resources/js/pagedown/.hg/undo.desc | 3 + resources/js/pagedown/.hg/undo.dirstate | 0 resources/js/pagedown/License | 21 + resources/js/pagedown/Markdown.Converter.js | 1628 +++++++++++ resources/js/pagedown/Markdown.Editor.js | 2301 ++++++++++++++++ .../js/pagedown/Markdown.Editor.modified.js | 2444 +++++++++++++++++ resources/js/pagedown/Markdown.Extra.js | 874 ++++++ themes/HumHub/css/theme.css | 1 - themes/HumHub/img/dynamic.php | 55 - themes/HumHub/img/wmd-buttons.png | Bin 0 -> 7465 bytes themes/HumHub/less/build.less | 2 + 156 files changed, 10417 insertions(+), 782 deletions(-) create mode 100644 protected/humhub/assets/PagedownConverterAsset.php create mode 100644 protected/humhub/components/rendering/LayoutRenderer.php create mode 100644 protected/humhub/components/rendering/MailLayoutRenderer.php create mode 100644 protected/humhub/components/rendering/Renderer.php create mode 100644 protected/humhub/components/rendering/ViewPathRenderer.php create mode 100644 protected/humhub/components/rendering/Viewable.php delete mode 100644 protected/humhub/libs/Viewable.php create mode 100644 protected/humhub/modules/activity/components/ActivityMailRenderer.php create mode 100644 protected/humhub/modules/activity/migrations/m161228_131023_rename_source_fields.php create mode 100644 protected/humhub/modules/admin/notifications/AdminNotificationCategory.php create mode 100644 protected/humhub/modules/admin/views/setting/notification.php create mode 100644 protected/humhub/modules/content/interfaces/ContentOwner.php delete mode 100644 protected/humhub/modules/content/interfaces/ContentTitlePreview.php create mode 100644 protected/humhub/modules/content/tests/codeception/fixtures/ContentContainerSettingFixture.php create mode 100644 protected/humhub/modules/content/tests/codeception/fixtures/data/contentcontainer_setting.php create mode 100644 protected/humhub/modules/friendship/notifications/FriendshipNotificationCategory.php create mode 100644 protected/humhub/modules/notification/components/MailNotificationTarget.php create mode 100644 protected/humhub/modules/notification/components/MailTargetRenderer.php create mode 100644 protected/humhub/modules/notification/components/MobileNotificationTarget.php create mode 100644 protected/humhub/modules/notification/components/NotificationCategory.php create mode 100644 protected/humhub/modules/notification/components/NotificationManager.php create mode 100644 protected/humhub/modules/notification/components/NotificationTarget.php create mode 100644 protected/humhub/modules/notification/components/WebNotificationTarget.php create mode 100644 protected/humhub/modules/notification/components/WebTargetRenderer.php create mode 100644 protected/humhub/modules/notification/models/forms/NotificationSettings.php create mode 100644 protected/humhub/modules/notification/tests/codeception/unit/category/NotificationCategoryTest.php create mode 100644 protected/humhub/modules/notification/tests/codeception/unit/category/notifications/SpecialNotification.php create mode 100644 protected/humhub/modules/notification/tests/codeception/unit/category/notifications/SpecialNotificationCategory.php create mode 100644 protected/humhub/modules/notification/tests/codeception/unit/category/notifications/TestNotification.php create mode 100644 protected/humhub/modules/notification/tests/codeception/unit/category/notifications/TestNotificationCategory.php create mode 100644 protected/humhub/modules/notification/tests/codeception/unit/rendering/MailTargetRenderTest.php create mode 100644 protected/humhub/modules/notification/tests/codeception/unit/rendering/WebTargetRenderTest.php create mode 100644 protected/humhub/modules/notification/tests/codeception/unit/rendering/notifications/TestedMailViewNotification.php create mode 100644 protected/humhub/modules/notification/tests/codeception/unit/rendering/views/layouts/notification/mail/plaintext/specialLayout.php create mode 100644 protected/humhub/modules/notification/tests/codeception/unit/rendering/views/layouts/notification/mail/specialLayout.php create mode 100644 protected/humhub/modules/notification/tests/codeception/unit/rendering/views/layouts/notification/specialLayout.php create mode 100644 protected/humhub/modules/notification/tests/codeception/unit/rendering/views/notification/mail/special.php create mode 100644 protected/humhub/modules/notification/tests/codeception/unit/rendering/views/notification/special.php create mode 100644 protected/humhub/modules/notification/views/notification/mail/default.php create mode 100644 protected/humhub/modules/notification/widgets/NotificationSettingsForm.php create mode 100644 protected/humhub/modules/notification/widgets/views/notificationSettingsForm.php create mode 100644 protected/humhub/modules/space/notifications/SpaceMemberNotificationCategory.php create mode 100644 protected/humhub/tests/config/test.php create mode 100644 resources/js/pagedown/.hg/00changelog.i create mode 100644 resources/js/pagedown/.hg/branch create mode 100644 resources/js/pagedown/.hg/cache/branchheads create mode 100644 resources/js/pagedown/.hg/cache/tags create mode 100644 resources/js/pagedown/.hg/dirstate create mode 100644 resources/js/pagedown/.hg/hgrc create mode 100644 resources/js/pagedown/.hg/requires create mode 100644 resources/js/pagedown/.hg/store/00changelog.i create mode 100644 resources/js/pagedown/.hg/store/00manifest.i create mode 100644 resources/js/pagedown/.hg/store/data/_l_i_c_e_n_s_e.txt.i create mode 100644 resources/js/pagedown/.hg/store/data/_markdown._converter.js.i create mode 100644 resources/js/pagedown/.hg/store/data/_markdown._editor.js.i create mode 100644 resources/js/pagedown/.hg/store/data/_markdown._sanitizer.js.i create mode 100644 resources/js/pagedown/.hg/store/data/_markdown.local.en.js.i create mode 100644 resources/js/pagedown/.hg/store/data/_markdown.local.fr.js.i create mode 100644 resources/js/pagedown/.hg/store/data/_r_e_a_d_m_e.txt.i create mode 100644 resources/js/pagedown/.hg/store/data/demo/browser/demo.css.i create mode 100644 resources/js/pagedown/.hg/store/data/demo/browser/demo.html.i create mode 100644 resources/js/pagedown/.hg/store/data/demo/node/demo.js.i create mode 100644 resources/js/pagedown/.hg/store/data/local/_markdown.local.fr.js.i create mode 100644 resources/js/pagedown/.hg/store/data/node-pagedown.js.i create mode 100644 resources/js/pagedown/.hg/store/data/package.json.i create mode 100644 resources/js/pagedown/.hg/store/data/resources/wmd-buttons.psd.i create mode 100644 resources/js/pagedown/.hg/store/data/wmd-buttons.png.i create mode 100644 resources/js/pagedown/.hg/store/fncache create mode 100644 resources/js/pagedown/.hg/store/phaseroots create mode 100644 resources/js/pagedown/.hg/store/undo create mode 100644 resources/js/pagedown/.hg/store/undo.phaseroots create mode 100644 resources/js/pagedown/.hg/undo.bookmarks create mode 100644 resources/js/pagedown/.hg/undo.branch create mode 100644 resources/js/pagedown/.hg/undo.desc create mode 100644 resources/js/pagedown/.hg/undo.dirstate create mode 100644 resources/js/pagedown/License create mode 100644 resources/js/pagedown/Markdown.Converter.js create mode 100644 resources/js/pagedown/Markdown.Editor.js create mode 100644 resources/js/pagedown/Markdown.Editor.modified.js create mode 100644 resources/js/pagedown/Markdown.Extra.js delete mode 100644 themes/HumHub/css/theme.css delete mode 100644 themes/HumHub/img/dynamic.php create mode 100644 themes/HumHub/img/wmd-buttons.png diff --git a/composer.json b/composer.json index 0178ae0d7b..e3b651acfa 100644 --- a/composer.json +++ b/composer.json @@ -51,7 +51,8 @@ "yiisoft/yii2-debug": "~2.0.0", "yiisoft/yii2-gii": "~2.0.0", "yiisoft/yii2-faker": "~2.0.0", - "yiisoft/yii2-apidoc": "~2.0.0" + "yiisoft/yii2-apidoc": "~2.0.0", + "googlecode/google-api-php-client": "0.6.*" }, "config": { "process-timeout": 1800, diff --git a/js/humhub/humhub.client.js b/js/humhub/humhub.client.js index f6cce33df8..f806b832ef 100644 --- a/js/humhub/humhub.client.js +++ b/js/humhub/humhub.client.js @@ -30,7 +30,12 @@ humhub.module('client', function(module, require, $) { }; Response.prototype.setError = function(errorThrown) { - this.error = errorThrown; + try { + this.error = JSON.parse(this.response); + } catch(e) {/* Nothing todo... */} + + this.error = this.error || {}; + this.errorThrown = errorThrown; this.validationError = (this.status === 400); return this; }; diff --git a/js/humhub/humhub.log.js b/js/humhub/humhub.log.js index 6feae8cb5e..9ad6c770ed 100644 --- a/js/humhub/humhub.log.js +++ b/js/humhub/humhub.log.js @@ -95,7 +95,10 @@ humhub.module('log', function (module, require, $) { if(msg instanceof Error && level >= TRACE_WARN) { details = msg; msg = this.getMessage(details.message, level, true); - } else if(msg.status && level >= TRACE_WARN) { + } else if(msg.error && msg.error.message) { // client.Response + details = msg; + msg = msg.error.message; + } else if(msg.status && level >= TRACE_WARN) { // client.Response.status details = msg; msg = this.getMessage(msg.status, level, true); } else if(object.isString(msg) || object.isNumber(msg)) { diff --git a/js/humhub/humhub.ui.additions.js b/js/humhub/humhub.ui.additions.js index 59999b537b..087d2b1180 100644 --- a/js/humhub/humhub.ui.additions.js +++ b/js/humhub/humhub.ui.additions.js @@ -10,6 +10,8 @@ humhub.module('ui.additions', function(module, require, $) { var event = require('event'); var object = require('util.object'); + var richtext = require('ui.richtext', true); + var _additions = {}; /** @@ -23,7 +25,7 @@ humhub.module('ui.additions', function(module, require, $) { */ var register = function(id, selector, handler, options) { options = options || {}; - + if(!_additions[id] || options.overwrite) { _additions[id] = { 'selector': selector, @@ -46,8 +48,8 @@ humhub.module('ui.additions', function(module, require, $) { * @returns {undefined} */ var applyTo = function(element, options) { - options = options || {}; - + options = options || {}; + var $element = (element instanceof $) ? element : $(element); $.each(_additions, function(id) { if(options.filter && !options.filter.indexOf(id)) { @@ -106,6 +108,38 @@ humhub.module('ui.additions', function(module, require, $) { }); }); + module.register('markdown', '[data-ui-markdown]', function($match) { + var converter = new Markdown.Converter(); + Markdown.Extra.init(converter); + $match.each(function() { + var $this = $(this); + + if($this.data('markdownProcessed')) { + return; + } + + + // Export all richtext features + var features = {}; + $this.find('[data-richtext-feature]').each(function() { + var $this = $(this); + features[$this.data('guid')] = $this.clone(); + $this.replaceWith($this.data('guid')); + }); + + var text = richtext.Richtext.plainText($this.clone()); + var result = converter.makeHtml(text); + + // Rewrite richtext feature + $.each(features, function(guid, $element) { + result = result.replace(guid.trim(), $('
').html($element).html()); + }); + + + $this.html(result).data('markdownProcessed', true); + }); + }); + $(document).on('click.humhub-ui-tooltip', function() { $('.tooltip').remove(); }); @@ -117,8 +151,8 @@ humhub.module('ui.additions', function(module, require, $) { // Activate placeholder text for older browsers (specially IE) /*this.register('placeholder','input, textarea', function($match) { - $match.placeholder(); - });*/ + $match.placeholder(); + });*/ // Replace the standard checkbox and radio buttons module.register('forms', ':checkbox, :radio', function($match) { @@ -130,10 +164,10 @@ humhub.module('ui.additions', function(module, require, $) { $match.loader(); }); }; - + var extend = function(id, handler, options) { options = options || {}; - + if(_additions[id]) { var addition = _additions[id]; if(options.prepend) { @@ -141,21 +175,21 @@ humhub.module('ui.additions', function(module, require, $) { } else { addition.handler = object.chain(addition.handler, addition.handler, handler); } - + if(options.selector && options.selector !== addition.selector) { - addition.selector += ','+options.selector; + addition.selector += ',' + options.selector; } - + if(options.applyOnInit) { module.apply('body', id); } - - } else if(options.selector){ + + } else if(options.selector) { options.extend = false; // Make sure we don't get caught in a loop somehow. module.register(id, options.selector, handler, options); } }; - + //TODO: additions.extend('id', handler); for extending existing additions. /** @@ -201,7 +235,7 @@ humhub.module('ui.additions', function(module, require, $) { } else if(!options) { options = {}; } - + node = (node instanceof $) ? node[0] : node; var observer = new MutationObserver(function(mutations) { diff --git a/js/humhub/humhub.ui.richtext.js b/js/humhub/humhub.ui.richtext.js index 5f77dd39f9..44892360ee 100644 --- a/js/humhub/humhub.ui.richtext.js +++ b/js/humhub/humhub.ui.richtext.js @@ -114,7 +114,7 @@ humhub.module('ui.richtext', function(module, require, $) { sel.addRange(range); } }; - + /** * Empty spans prevent text deletions in some browsers, so we have to get sure there are no empty spans present. * @param {type} $node @@ -259,17 +259,17 @@ humhub.module('ui.richtext', function(module, require, $) { tooltip = tooltip || this.options.disabledText; this.$.removeAttr('contenteditable').attr({ disabled: 'disabled', - title : tooltip, + title: tooltip, }).tooltip({ - placement : 'bottom' + placement: 'bottom' }); }; - + Richtext.prototype.clear = function() { this.$.html(''); this.checkPlaceholder(); }; - + Richtext.prototype.focus = function() { this.$.trigger('focus'); }; @@ -294,7 +294,17 @@ humhub.module('ui.richtext', function(module, require, $) { } }); - var html = $clone.html(); + return Richtext.plainText($clone); + }; + + Richtext.plainText = function(element, options) { + options = options || {}; + var $element = element instanceof $ ? element : $(element); + + var html = $element.html(); + + // remove all line breaks + html = html.replace(/(?:\r\n|\r|\n)/g, ""); // replace html space html = html.replace(/\ /g, ' '); @@ -309,18 +319,17 @@ humhub.module('ui.richtext', function(module, require, $) { html = html.replace(/\

\\<\/p>/g, '
'); html = html.replace(/\<\/p>/g, '
'); - // remove all line breaks - html = html.replace(/(?:\r\n|\r|\n)/g, ""); - // At.js adds a zwj at the end of each mentioning - html = html.replace(/\u200d/g,''); + html = html.replace(/\u200d/g, ''); // replace all
with new line break - $clone.html(html.replace(/\/g, '\n')); + html = html.replace(/\/g, '\n'); // return plain text without html tags + var $clone = (options.clone) ? $element.clone() : $element; + $clone.html(html); return $clone.text().trim(); - }; + } Richtext.features = {}; diff --git a/js/humhub/legacy/jquery.flatelements.js b/js/humhub/legacy/jquery.flatelements.js index fbcda16ec4..cd106e4028 100644 --- a/js/humhub/legacy/jquery.flatelements.js +++ b/js/humhub/legacy/jquery.flatelements.js @@ -110,8 +110,10 @@ // assign label to checkbox $this.parent().attr('for', $this.attr('id')); + var $checkbox = $('

').attr('style', $this.attr('style')); + // add new checkbox element - $this.parent().append('
'); + $this.parent().append($checkbox); } } diff --git a/less/markdown.less b/less/markdown.less index d531cc86eb..a6b0450c79 100644 --- a/less/markdown.less +++ b/less/markdown.less @@ -1,78 +1,323 @@ // Markdown .markdown-render { - h1, - h2, - h3, - h4, - h5, - h6 { - font-weight: bold !important; - } - h1 { - font-size: 28px !important; - } - h2 { - font-size: 24px !important; - } - h3 { - font-size: 18px !important; - } - h4 { - font-size: 16px !important; - } - h5 { - font-size: 14px !important; - } - h6 { - color: #999; - font-size: 14px !important; - } - pre { - padding: 0; - border: none; - border-radius: 3px; - code { - padding: 10px; - border-radius: 3px; - font-size: 12px !important; + h1, + h2, + h3, + h4, + h5, + h6 { + font-weight: bold !important; } - } - a, - a:visited { - background-color: inherit; - text-decoration: none; - color: @info !important; - } - img { - max-width: 100%; - display: table-cell !important; - } - - table { - width: 100%; - th { - font-size: 13px; - font-weight: 700; - color: @font3; + h1 { + font-size: 28px !important; + } + h2 { + font-size: 24px !important; + } + h3 { + font-size: 18px !important; + } + h4 { + font-size: 16px !important; + } + h5 { + font-size: 14px !important; + } + h6 { + color: #999; + font-size: 14px !important; + } + pre { + padding: 0; + border: none; + border-radius: 3px; + code { + padding: 10px; + border-radius: 3px; + font-size: 12px !important; + } + } + a, + a:visited { + background-color: inherit; + text-decoration: none; + color: @info !important; + } + img { + max-width: 100%; + display: table-cell !important; } - thead { - tr { - border-bottom: 1px solid @background3; - } - } + table { + width: 100%; + th { + font-size: 13px; + font-weight: 700; + color: @font3; + } - tbody tr td, thead tr th { - border: 1px solid @background3 !important; - padding: 4px; + thead { + tr { + border-bottom: 1px solid @background3; + } + } + + tbody tr td, thead tr th { + border: 1px solid @background3 !important; + padding: 4px; + } } - } } .md-editor.active { - border: 2px solid @info !important; + border: 2px solid @info !important; } .md-editor textarea { - padding: 10px !important; -} \ No newline at end of file + padding: 10px !important; +} + +[data-ui-markdown] { + + word-break: break-all; + + h1, h2, h3, h4, h5, h6 { + margin: 0 0 1em 0; + text-align: start; + } + + h1 { + font-size: 2.6em !important; + } + + h2 { + font-size: 2.15em !important; + } + + h3 { + font-size: 1.7em !important; + } + + h4 { + font-size: 1.25em !important; + } + + h5 { + font-size: 1em !important; + } + + h6 { + font-size: .85em !important; + } + + p, pre, blockquote { + margin: 0 0 1.1em; + } + + blockquote { + border-left-width: 10px; + background-color: rgba(128,128,128,0.05); + border-top-right-radius: 5px; + border-bottom-right-radius: 5px; + padding: 15px 20px; + font-size: 1em; + border-left: 5px solid #888888; + + } + + table { + margin-bottom: 20px; + max-width: 100%; + background-color: transparent; + border-collapse: collapse; + border-spacing: 0px; + } + + table caption+thead tr:first-child th, + table caption+thead tr:first-child td, + table colgroup+thead tr:first-child th, + table colgroup+thead tr:first-child td, + table thead:first-child tr:first-child th, + table thead:first-child tr:first-child td { + border-top: 0px; + } + + table thead th { + vertical-align: bottom; + } + + table th { + font-weight: bold; + text-align: left; + } + + table th, table td { + padding: 8px; + line-height: 20px; + vertical-align: top; + border-top: 1px solid #ddd; + } + + dt, dd { + margin-top: 5px; + margin-bottom: 5px; + line-height: 1.45; + } + + dt { + font-weight: bold; + } + + dd { + margin-left: 40px; + } + + pre { + text-align: start; + border: 0; + padding: 10px 20px; + border-radius: 5px; + code { + white-space: pre !important; + } + } + + + blockquote ul:last-child, blockquote ol:last-child { + margin-bottom: 0px; + } + + ul, ol { + margin-top: 0; + margin-bottom: 10.5px; + } + + .footnote { + vertical-align: top; + position: relative; + top: -0.5em; + font-size: .8em; + } +} + + +blockquote { + border-left: 2px dotted #888; + padding-left: 5px; + background: #d0f0ff; +} + +.wmd-panel +{ + //margin-left: 25%; + //margin-right: 25%; + //width: 50%; + min-width: 500px; +} + +.wmd-button-bar +{ + width: 100%; + background-color: Silver; +} + +.wmd-input +{ + height: 300px; + width: 100%; + background-color: Gainsboro; + border: 1px solid DarkGray; +} + +.wmd-preview +{ + //background-color: #c0e0ff; +} + +.wmd-button-row +{ + position: relative; + margin-left: 5px; + margin-right: 5px; + margin-bottom: 5px; + margin-top: 10px; + padding: 0px; + height: 20px; +} + +.wmd-spacer +{ + width: 1px; + height: 20px; + margin-left: 14px; + + position: absolute; + background-color: Silver; + display: inline-block; + list-style: none; +} + +.wmd-button { + width: 20px; + height: 20px; + padding-left: 2px; + padding-right: 3px; + position: absolute; + display: inline-block; + list-style: none; + cursor: pointer; +} + +.wmd-button > span { + background-image: url(../img/wmd-buttons.png); + background-repeat: no-repeat; + background-position: 0px 0px; + width: 20px; + height: 20px; + display: inline-block; +} + +.wmd-spacer1 +{ + left: 50px; +} +.wmd-spacer2 +{ + left: 175px; +} +.wmd-spacer3 +{ + left: 300px; +} + + + + +.wmd-prompt-background +{ + background-color: Black; +} + +.wmd-prompt-dialog +{ + border: 1px solid #999999; + background-color: #F5F5F5; +} + +.wmd-prompt-dialog > div { + font-size: 0.8em; + font-family: arial, helvetica, sans-serif; +} + + +.wmd-prompt-dialog > form > input[type="text"] { + border: 1px solid #999999; + color: black; +} + +.wmd-prompt-dialog > form > input[type="button"]{ + border: 1px solid #888888; + font-family: trebuchet MS, helvetica, sans-serif; + font-size: 0.8em; + font-weight: bold; +} diff --git a/protected/config/web.php b/protected/config/web.php index 1e00c1aa8f..e6c865bbd5 100644 --- a/protected/config/web.php +++ b/protected/config/web.php @@ -5,7 +5,7 @@ return [ 'modules' => [ 'debug' => [ 'class' => 'yii\debug\Module', - //'allowedIPs' => ['*'], + 'allowedIPs' => ['*'], ], ], ]; diff --git a/protected/humhub/assets/AppAsset.php b/protected/humhub/assets/AppAsset.php index 8cce0271a0..a1019fb831 100755 --- a/protected/humhub/assets/AppAsset.php +++ b/protected/humhub/assets/AppAsset.php @@ -92,6 +92,7 @@ class AppAsset extends AssetBundle 'humhub\assets\NProgressAsset', 'humhub\assets\IE9FixesAsset', 'humhub\assets\IEFixesAsset', + 'humhub\assets\PagedownConverterAsset', ]; } diff --git a/protected/humhub/assets/PagedownConverterAsset.php b/protected/humhub/assets/PagedownConverterAsset.php new file mode 100644 index 0000000000..d93a97bc4a --- /dev/null +++ b/protected/humhub/assets/PagedownConverterAsset.php @@ -0,0 +1,40 @@ +getNotifications()); + } } diff --git a/protected/humhub/components/SettingsManager.php b/protected/humhub/components/SettingsManager.php index 2953854cd8..bcee4af76c 100644 --- a/protected/humhub/components/SettingsManager.php +++ b/protected/humhub/components/SettingsManager.php @@ -48,21 +48,27 @@ class SettingsManager extends BaseSettingsManager } /** - * Returns ContentContainerSettingsManager for current logged in user + * Returns ContentContainerSettingsManager for the given $user or current logged in user * @return ContentContainerSettingsManager */ - public function user() + public function user($user = null) { - return $this->contentContainer(Yii::$app->user->getIdentity()); + if(!$user) { + $user = Yii::$app->user->getIdentity(); + } + + return $this->contentContainer($user); } /** - * Returns ContentContainerSettingsManager for current logged in user + * Returns ContentContainerSettingsManager for the given $space or current controller space * @return ContentContainerSettingsManager */ - public function space() + public function space($space = null) { - if (Yii::$app->controller instanceof \humhub\modules\content\components\ContentContainerController) { + if($space != null) { + return $this->contentContainer($space); + } elseif (Yii::$app->controller instanceof \humhub\modules\content\components\ContentContainerController) { if (Yii::$app->controller->contentContainer instanceof \humhub\modules\space\models\Space) { return $this->contentContainer(Yii::$app->controller->contentContainer); } diff --git a/protected/humhub/components/SocialActivity.php b/protected/humhub/components/SocialActivity.php index f28290b755..2678b0fdce 100644 --- a/protected/humhub/components/SocialActivity.php +++ b/protected/humhub/components/SocialActivity.php @@ -8,25 +8,27 @@ namespace humhub\components; -use humhub\modules\notification\models\Notification; -use humhub\modules\content\components\ContentActiveRecord; +use Yii; +use yii\helpers\Html; use humhub\modules\content\components\ContentContainerActiveRecord; -use humhub\modules\content\components\ContentAddonActiveRecord; -use humhub\libs\Viewable; +use humhub\modules\space\models\Space; +use humhub\modules\content\interfaces\ContentOwner; +use humhub\widgets\RichText; /** - * Name (SocialEvent/NetworkEvent/SocialActivity/BaseEvent) - * - * This class represents an social activity triggered within the network. - * An activity instance can be linked to an $originator user, which performed the activity. + * This class represents a social Activity triggered within the network. * - * The activity mainly provides functions for rendering the output for different channels as - * web, mail or plain-text. + * A SocialActivity can be assigned with an originator User, which triggered the activity and a source ActiveRecord. + * The source is used to connect the SocialActivity to a related Content, ContentContainerActiveRecord or any other + * ActiveRecord. + * + * Since SocialActivities need to be rendered in most cases it implements the humhub\components\rendering\Viewable interface and provides + * a default implementation of the getViewParams function. * * @since 1.1 * @author buddha */ -abstract class SocialActivity extends Viewable +abstract class SocialActivity extends \yii\base\Object implements rendering\Viewable { /** @@ -43,42 +45,168 @@ abstract class SocialActivity extends Viewable */ public $source; - /** - * The content container this activity belongs to. - * - * If the source object is a type of Content/ContentAddon or ContentContainer the container - * will be automatically set. - * - * @var ContentContainerActiveRecord - */ - public $container = null; - /** * @var string the module id which this activity belongs to (required) */ public $moduleId = ""; - + + /** - * The notification record this notification belongs to - * - * @var Notification + * An SocialActivity can be represented in the database as ActiveRecord. + * By defining the $recordClass an ActiveRecord will be created automatically within the + * init function. + * + * @var \yii\db\ActiveRecord The related record for this activitiy */ public $record; + + /** + * @var string Record class used for instantiation. + */ + public $recordClass; + + /** + * @var string view name used for rendering the activity + */ + public $viewName = 'default.php'; + + public function init() + { + parent::init(); + if($this->recordClass) { + $this->record = Yii::createObject($this->recordClass, [ + 'class' => $this->className() + ]); + } + } + + /** + * Static initializer should be prefered over new initialization, since it makes use + * of Yii::createObject dependency injection/configuration. + * + * @return \humhub\components\SocialActivity + */ + public static function instance($options = []) + { + return Yii::createObject(static::class, $options); + } + + /** + * Builder function for the originator. + * + * @param type $originator + * @return \humhub\components\SocialActivity + */ + public function from($originator) + { + $this->originator = $originator; + return $this; + } + + /** + * Builder function for the source. + * @param type $source + * @return \humhub\components\SocialActivity + */ + public function about($source) + { + $this->source = $source; + $this->record->source_pk = $this->source->getPrimaryKey(); + $this->record->source_class = $this->source->className(); + return $this; + } + + /** + * @inheritdoc + */ + public function getViewName() + { + // If no suffix is given, we assume a php file. + if(!strpos($this->viewName, '.')) { + return $this->viewName. '.php'; + } else { + return $this->viewName; + } + } /** * @inheritdoc */ - protected function getViewParams($params = []) + public function getViewParams($params = []) { - $params['originator'] = $this->originator; - $params['source'] = $this->source; - $params['contentContainer'] = $this->container; - $params['record'] = $this->record; - if (!isset($params['url'])) { - $params['url'] = $this->getUrl(); + $result = [ + 'originator' => $this->originator, + 'source' => $this->source, + 'contentContainer' => $this->getContentContainer(), + 'space' => $this->getSpace(), + 'record' => $this->record, + 'url' => $this->getUrl(), + 'viewable' => $this, + 'html' => $this->html(), + 'text' => $this->text() + ]; + + return \yii\helpers\ArrayHelper::merge($result, $params); + } + + /** + * Returns the related content instance in case the source is of type ContentOwner. + * + * @return \humhub\modules\content\models\Content Content ActiveRecord or null if not related to a ContentOwner source + */ + public function getContent() + { + if ($this->hasContent()) { + return $this->source->content; } - return $params; + return null; + } + + /** + * @return Space related space instance in case the activity source is an related contentcontainer of type space, otherwise null + */ + public function getSpace() + { + $container = $this->getContentContainer(); + return ($container instanceof Space) ? $container : null; + } + + /** + * @return integer related space id in case the activity source is an related contentcontainer of type space, otherwise null + */ + public function getSpaceId() + { + $space = $this->getSpace(); + return ($space) ? $space->id : null; + } + + /** + * Determines if this activity is related to a content. This is the case if the activitiy source + * is of type ContentOwner. + * + * @return boolean true if this activity is related to a ContentOwner else false + */ + public function hasContent() + { + return $this->source instanceof ContentOwner; + } + + /** + * Determines if the activity source is related to an ContentContainer. + * This is the case if the source is either a ContentContainerActiveRecord itself or a ContentOwner. + * + * @return ContentContainerActiveRecord + */ + public function getContentContainer() + { + if ($this->source instanceof ContentContainerActiveRecord) { + return $this->source; + } else if ($this->hasContent()) { + return $this->getContent()->getContainer(); + } + + return null; } /** @@ -91,8 +219,8 @@ abstract class SocialActivity extends Viewable { $url = '#'; - if ($this->source instanceof ContentActiveRecord || $this->source instanceof ContentAddonActiveRecord) { - $url = $this->source->content->getUrl(); + if ($this->hasContent()) { + $url = $this->getContent()->getUrl(); } elseif ($this->source instanceof ContentContainerActiveRecord) { $url = $this->source->getUrl(); } @@ -105,6 +233,55 @@ abstract class SocialActivity extends Viewable return $url; } + /** + * @inheritdoc + */ + public function text() + { + $html = $this->html(); + return !empty($html) ? strip_tags($html) : null; + } + + /** + * @inheritdoc + */ + public function html() + { + return null; + } + + /** + * @inheritdoc + */ + public function json() + { + return \yii\helpers\Json::encode($this->asArray()); + } + + /** + * Returns an array representation of this notification. + */ + public function asArray() + { + $result = [ + 'class' => $this->className(), + 'text' => $this->text(), + 'html' => $this->html() + ]; + + if($this->originator) { + $result['originator_id'] = $this->originator->id; + } + + if ($this->source) { + $result['source_class'] = $this->source->className(); + $result['source_pk'] = $this->source->getPrimaryKey(); + $result['space_id'] = $this->source->getSpaceId(); + } + + return $result; + } + /** * Build info text about a content * @@ -114,11 +291,11 @@ abstract class SocialActivity extends Viewable * @param Content $content * @return string */ - public function getContentInfo(\humhub\modules\content\interfaces\ContentTitlePreview $content) + public function getContentInfo(ContentOwner $content) { - return \yii\helpers\Html::encode($content->getContentName()) . + return Html::encode($content->getContentName()) . ' "' . - \humhub\widgets\RichText::widget(['text' => $content->getContentDescription(), 'minimal' => true, 'maxLength' => 60]) . '"'; + RichText::widget(['text' => $content->getContentDescription(), 'minimal' => true, 'maxLength' => 60]) . '"'; } } diff --git a/protected/humhub/components/rendering/LayoutRenderer.php b/protected/humhub/components/rendering/LayoutRenderer.php new file mode 100644 index 0000000000..2a9ed80387 --- /dev/null +++ b/protected/humhub/components/rendering/LayoutRenderer.php @@ -0,0 +1,59 @@ +/myView.php' + * + * where viewPath can also be provided as a Yii alias. + * + * The rendered view will be embeded into the given $layout which should point to the layout file + * and can also be provided as a Yii alias e.g: + * + * '@myModule/views/layouts/myLayout.php' + * + * @author buddha + */ +class LayoutRenderer extends ViewPathRenderer +{ + + /** + * @var string layout file path + */ + public $layout; + + /** + * @inheritdoc + */ + public function render(Viewable $viewable, $params = []) + { + // Render the view itself + $viewParams = $viewable->getViewParams($params); + + if(!isset($viewParams['content'])) { + $viewParams['content'] = parent::renderView($viewable, $viewParams); + } + + $layout = $this->getLayout($viewable); + + // Embed view into layout if provided + if ($layout) { + return Yii::$app->getView()->renderFile($layout, $viewParams, $viewable); + } else { + return $viewParams['content']; + } + } + + protected function getLayout(Viewable $viewable) + { + return $this->layout; + } + +} diff --git a/protected/humhub/components/rendering/MailLayoutRenderer.php b/protected/humhub/components/rendering/MailLayoutRenderer.php new file mode 100644 index 0000000000..79e18de87b --- /dev/null +++ b/protected/humhub/components/rendering/MailLayoutRenderer.php @@ -0,0 +1,42 @@ + $this->getTextLayout($viewable) + ]); + + $params['content'] = $viewable->text(); + + return strip_tags($textRenderer->render($viewable, $params)); + } + + public function getTextLayout(Viewable $viewable ) + { + return $this->textLayout; + } +} diff --git a/protected/humhub/components/rendering/Renderer.php b/protected/humhub/components/rendering/Renderer.php new file mode 100644 index 0000000000..e6c02649ab --- /dev/null +++ b/protected/humhub/components/rendering/Renderer.php @@ -0,0 +1,25 @@ +viewName to determine the target view and + * forward the given $params to $viewable->getViewParams($params). By doing so, the + * $params can be used to overwrite the default view parameter of $viewable. + * + * @param \humhub\components\rendering\Viewable $viewable + * @param type $params + */ + public function render(Viewable $viewable, $params = []); +} diff --git a/protected/humhub/components/rendering/ViewPathRenderer.php b/protected/humhub/components/rendering/ViewPathRenderer.php new file mode 100644 index 0000000000..0663beaee6 --- /dev/null +++ b/protected/humhub/components/rendering/ViewPathRenderer.php @@ -0,0 +1,77 @@ +renderView($viewable, $viewable->getViewParams($params)); + } + + /** + * Helper function for rendering a Viewable with the given viewParams. + * + * @param \humhub\components\rendering\Viewable $viewable + * @param type $viewParams + * @return type + */ + public function renderView(Viewable $viewable, $viewParams) + { + $viewFile = $this->getViewFile($viewable); + return Yii::$app->getView()->renderFile($viewFile, $viewParams, $viewable); + } + + /** + * Returnes the viewFile of the given Viewable. + * + * @param \humhub\components\rendering\Viewable $viewable + * @return type + */ + public function getViewFile(Viewable $viewable) + { + return $this->getViewPath($viewable) . '/' . $viewable->getViewName(); + } + + /** + * Returns the directory containing the view files for this event. + * The default implementation returns the 'views' subdirectory under the directory containing the notification class file. + * @return string the directory containing the view files for this notification. + */ + public function getViewPath(Viewable $viewable) + { + if ($this->viewPath !== null) { + return Yii::getAlias($this->viewPath); + } + + $class = new \ReflectionClass($viewable); + return dirname(dirname($class->getFileName())) . DIRECTORY_SEPARATOR . 'views'; + } + +} diff --git a/protected/humhub/components/rendering/Viewable.php b/protected/humhub/components/rendering/Viewable.php new file mode 100644 index 0000000000..a7335feea5 --- /dev/null +++ b/protected/humhub/components/rendering/Viewable.php @@ -0,0 +1,39 @@ + [ 'class' => '\humhub\components\ModuleManager' ], + 'notification' => [ + 'class' => 'humhub\modules\notification\components\NotificationManager', + 'targets' => [ + [ + 'class' => 'humhub\modules\notification\components\WebNotificationTarget', + 'renderer' => ['class' => 'humhub\modules\notification\components\WebTargetRenderer'] + ], + [ + 'class' => 'humhub\modules\notification\components\MailNotificationTarget', + 'renderer' => ['class' => 'humhub\modules\notification\components\MailTargetRenderer'] + ], + //['class' => '\humhub\modules\notification\components\MobileNotificationTarget'] + ] + ], 'log' => [ 'traceLevel' => YII_DEBUG ? 3 : 0, 'targets' => [ diff --git a/protected/humhub/docs/CHANGELOG.md b/protected/humhub/docs/CHANGELOG.md index 21eab8af38..8c1cde24e3 100644 --- a/protected/humhub/docs/CHANGELOG.md +++ b/protected/humhub/docs/CHANGELOG.md @@ -28,4 +28,5 @@ HumHub Change Log - Enh: Picker widgets rewrite (UserPicker/SpacePicker/MultiselectDropdown). (buddha) - Enh: Richtext widget rewrite. (buddha) - Enh: Removed almost all inline JS blocks. (buddha) -- Enh: StreamAction now uses flexible StreamQuery Model. +- Enh: StreamAction now uses flexible StreamQuery Model. (buddha) +- Enh: Post markdown support. (buddha) diff --git a/protected/humhub/libs/Viewable.php b/protected/humhub/libs/Viewable.php deleted file mode 100644 index 41e22077af..0000000000 --- a/protected/humhub/libs/Viewable.php +++ /dev/null @@ -1,158 +0,0 @@ -originator; - $params['source'] = $this->source; - $params['contentContainer'] = $this->container; - $params['record'] = $this->record; - $params['url'] = $this->getUrl(); - - return $params; - } - - /** - * Renders the notification - * - * @return string - */ - public function render($mode = self::OUTPUT_WEB, $params = []) - { - $viewFile = $this->getViewFile($mode); - $viewParams = $this->getViewParams($params); - - $result = Yii::$app->getView()->renderFile($viewFile, $viewParams, $this); - - if ($mode == self::OUTPUT_TEXT) { - return strip_tags($result); - } - - $viewParams['content'] = $result; - return Yii::$app->getView()->renderFile($this->getLayoutFile($mode), $viewParams, $this); - } - - /** - * Returns the correct view file - * - * @param string $mode the output mode - * @return string the view file - */ - protected function getViewFile($mode) - { - $viewFile = $this->getViewPath() . '/' . $this->viewName . '.php'; - $alternativeViewFile = ""; - - // Lookup alternative view file based on view mode - if ($mode == self::OUTPUT_MAIL) { - $alternativeViewFile = $this->getViewPath() . '/mail/' . $this->viewName . '.php'; - } elseif ($mode === self::OUTPUT_MAIL_PLAINTEXT) { - $alternativeViewFile = $this->getViewPath() . '/mail/plaintext/' . $this->viewName . '.php'; - } - - if ($alternativeViewFile != "" && file_exists($alternativeViewFile)) { - $viewFile = $alternativeViewFile; - } - - return $viewFile; - } - - /** - * Returns the layout file - * - * @param string $mode the output mode - * @return string the layout file - */ - protected function getLayoutFile($mode) - { - if ($mode == self::OUTPUT_MAIL_PLAINTEXT) { - return $this->layoutMailPlaintext; - } elseif ($mode == self::OUTPUT_MAIL) { - return $this->layoutMail; - } - - return $this->layoutWeb; - } - - /** - * Returns the directory containing the view files for this event. - * The default implementation returns the 'views' subdirectory under the directory containing the notification class file. - * @return string the directory containing the view files for this notification. - */ - public function getViewPath() - { - if ($this->viewPath !== null) { - return Yii::getAlias($this->viewPath); - } - - $class = new \ReflectionClass($this); - return dirname($class->getFileName()) . DIRECTORY_SEPARATOR . 'views'; - } - -} diff --git a/protected/humhub/modules/activity/components/ActivityMailRenderer.php b/protected/humhub/modules/activity/components/ActivityMailRenderer.php new file mode 100644 index 0000000000..fee630d57f --- /dev/null +++ b/protected/humhub/modules/activity/components/ActivityMailRenderer.php @@ -0,0 +1,23 @@ +visibility === null) { - $this->visibility = \humhub\modules\content\models\Content::VISIBILITY_PRIVATE; - } - parent::init(); } + + public function render($params = array()) + { + + } /** * @inheritdoc @@ -68,9 +75,13 @@ abstract class BaseActivity extends \humhub\components\SocialActivity public function getViewParams($params = []) { $params['clickable'] = $this->clickable; - return parent::getViewParams($params); } + + public function save() + { + return $this->record->save(); + } /** * Creates an activity model and determines the contentContainer/visibility @@ -87,40 +98,51 @@ abstract class BaseActivity extends \humhub\components\SocialActivity throw new \yii\base\InvalidConfigException("Invalid source object given!"); } - if ($this->container == null) { - $this->container = $this->getContentContainerFromSource(); - - if ($this->container == null) { - throw new \yii\base\InvalidConfigException("Could not determine content container for activity!"); - } - } - $this->saveModelInstance(); } - - protected function getContentContainerFromSource() + + /** + * @inheritdoc + */ + public function from($originator) { - if ($this->hasContentSource()) { - return $this->source->content->container; - } elseif ($this->source instanceof ContentContainerActiveRecord) { - return $this->source; - } + parent::from($originator); + $this->record->content->created_by = $originator->id; + return $this; + } + + /** + * @inheritdoc + */ + public function about($source) + { + parent::from($source); + $this->record->content->visibility = $this->getContentVisibility(); + return $this; } - protected function hasContentSource() + + /** + * Builder function for setting ContentContainerActiveRecord + * @param \humhub\modules\content\components\ContentContainerActiveRecord $container + */ + public function container($container) { - return $this->source instanceof ContentActiveRecord || $this->source instanceof ContentAddonActiveRecord; + $this->record->content->container = $container; + return $this; } private function saveModelInstance() { - $model = new Activity(); - $model->class = $this->className(); - $model->module = $this->moduleId; - $model->object_model = $this->source->className(); - $model->object_id = $this->source->getPrimaryKey(); - $model->content->container = $this->container; - $model->content->visibility = $this->getContentVisibility(); + $this->record->source_class = $this->source->className(); + $this->record->source_pk = $this->source->getPrimaryKey(); + $this->record->content->visibility = $this->getContentVisibility(); + + if (!$this->content->container) { + $model->content->container = $this->getContentContainer(); + + } + $model->content->created_by = $this->getOriginatorId(); if ($model->content->created_by == null) { @@ -134,7 +156,7 @@ abstract class BaseActivity extends \humhub\components\SocialActivity protected function getContentVisibility() { - return $this->hasContentSource() ? $this->source->content->visibility : $this->visibility; + return $this->hasContent() ? $this->getContent()->visibility : $this->visibility; } protected function getOriginatorId() diff --git a/protected/humhub/modules/activity/migrations/m161228_131023_rename_source_fields.php b/protected/humhub/modules/activity/migrations/m161228_131023_rename_source_fields.php new file mode 100644 index 0000000000..7454be1e21 --- /dev/null +++ b/protected/humhub/modules/activity/migrations/m161228_131023_rename_source_fields.php @@ -0,0 +1,30 @@ +renameColumn('activity', 'object_model', 'source_class'); + $this->renameColumn('activity', 'object_id', 'source_pk'); + } + + public function down() + { + echo "m161228_131023_rename_source_fields cannot be reverted.\n"; + + return false; + } + + /* + // Use safeUp/safeDown to run migration code within a transaction + public function safeUp() + { + } + + public function safeDown() + { + } + */ +} diff --git a/protected/humhub/modules/activity/models/Activity.php b/protected/humhub/modules/activity/models/Activity.php index cbc86117cd..4b85a04417 100644 --- a/protected/humhub/modules/activity/models/Activity.php +++ b/protected/humhub/modules/activity/models/Activity.php @@ -36,6 +36,8 @@ class Activity extends ContentActiveRecord return [ [ 'class' => \humhub\components\behaviors\PolymorphicRelation::className(), + 'classAttribute' => 'source_class', + 'pkAttribute' => 'source_pk', 'mustBeInstanceOf' => [ \yii\db\ActiveRecord::className(), ] @@ -57,9 +59,9 @@ class Activity extends ContentActiveRecord public function rules() { return [ - [['object_id'], 'integer'], + [['source_pk'], 'integer'], [['class'], 'string', 'max' => 100], - [['module', 'object_model'], 'string', 'max' => 100] + [['module', 'source_class'], 'string', 'max' => 100] ]; } diff --git a/protected/humhub/modules/admin/Module.php b/protected/humhub/modules/admin/Module.php index d8ea9629af..16143091b7 100644 --- a/protected/humhub/modules/admin/Module.php +++ b/protected/humhub/modules/admin/Module.php @@ -80,7 +80,7 @@ class Module extends \humhub\components\Module { if(Yii::$app->user->isAdmin()) { return [ - 'humhub\modules\user\notifications\NewVersionAvailable' + 'humhub\modules\admin\notifications\NewVersionAvailable' ]; } return []; diff --git a/protected/humhub/modules/admin/controllers/SettingController.php b/protected/humhub/modules/admin/controllers/SettingController.php index 5f0aab1956..2039b50a16 100644 --- a/protected/humhub/modules/admin/controllers/SettingController.php +++ b/protected/humhub/modules/admin/controllers/SettingController.php @@ -12,6 +12,7 @@ use Yii; use humhub\libs\ThemeHelper; use humhub\models\UrlOembed; use humhub\modules\admin\components\Controller; +use humhub\modules\notification\models\forms\NotificationSettings; /** * SettingController @@ -138,7 +139,20 @@ class SettingController extends Controller $this->view->saved(); } - return $this->render('mailing', array('model' => $form)); + return $this->render('mailing', ['model' => $form]); + } + + /** + * Notification Mailing Settings + */ + public function actionNotification() + { + $form = new NotificationSettings(); + if ($form->load(Yii::$app->request->post()) && $form->validate() && $form->save()) { + $this->view->saved(); + } + + return $this->render('notification', ['model' => $form]); } /** diff --git a/protected/humhub/modules/admin/models/forms/UserEditForm.php b/protected/humhub/modules/admin/models/forms/UserEditForm.php index f1fb8b5e6a..5896829aa5 100644 --- a/protected/humhub/modules/admin/models/forms/UserEditForm.php +++ b/protected/humhub/modules/admin/models/forms/UserEditForm.php @@ -1,11 +1,4 @@ Html::tag('strong', $this->getLatestHumHubVersion())]); } diff --git a/protected/humhub/modules/admin/views/setting/notification.php b/protected/humhub/modules/admin/views/setting/notification.php new file mode 100644 index 0000000000..e01fc49822 --- /dev/null +++ b/protected/humhub/modules/admin/views/setting/notification.php @@ -0,0 +1,22 @@ + + +
+

+
+
+ +
+ +
+ + $model, + 'form' => $form + ]) ?> + +
+ diff --git a/protected/humhub/modules/admin/widgets/SettingsMenu.php b/protected/humhub/modules/admin/widgets/SettingsMenu.php index 115f4db94f..0fe2376891 100644 --- a/protected/humhub/modules/admin/widgets/SettingsMenu.php +++ b/protected/humhub/modules/admin/widgets/SettingsMenu.php @@ -26,42 +26,50 @@ class SettingsMenu extends \humhub\widgets\BaseMenu { $canEditSettings = Yii::$app->user->can(new \humhub\modules\admin\permissions\ManageSettings()); - $this->addItem(array( + $this->addItem([ 'label' => Yii::t('AdminModule.widgets_AdminMenuWidget', 'General'), 'url' => Url::toRoute('/admin/setting/index'), 'icon' => '', 'sortOrder' => 100, 'isActive' => (Yii::$app->controller->module && Yii::$app->controller->module->id == 'admin' && Yii::$app->controller->id == 'setting' && Yii::$app->controller->action->id == 'basic'), 'isVisible' => $canEditSettings - )); + ]); - $this->addItem(array( + $this->addItem([ 'label' => Yii::t('AdminModule.widgets_AdminMenuWidget', 'Appearance'), 'url' => Url::toRoute('/admin/setting/design'), 'icon' => '', 'sortOrder' => 200, 'isActive' => (Yii::$app->controller->module && Yii::$app->controller->module->id == 'admin' && Yii::$app->controller->id == 'setting' && Yii::$app->controller->action->id == 'design'), 'isVisible' => $canEditSettings - )); + ]); - $this->addItem(array( + $this->addItem([ 'label' => Yii::t('AdminModule.widgets_AdminMenuWidget', 'E-Mails'), 'url' => Url::toRoute('/admin/setting/mailing'), 'icon' => '', 'sortOrder' => 300, 'isActive' => (Yii::$app->controller->module && Yii::$app->controller->module->id == 'admin' && Yii::$app->controller->id == 'setting' && (Yii::$app->controller->action->id == 'mailing' || Yii::$app->controller->action->id == 'mailing-server')), 'isVisible' => $canEditSettings - )); + ]); + + $this->addItem([ + 'label' => Yii::t('AdminModule.widgets_AdminMenuWidget', 'Notifications'), + 'url' => Url::toRoute('/admin/setting/notification'), + 'icon' => '', + 'sortOrder' => 400, + 'isActive' => (Yii::$app->controller->module && Yii::$app->controller->module->id == 'admin' && Yii::$app->controller->id == 'setting' && (Yii::$app->controller->action->id == 'notification')), + 'isVisible' => $canEditSettings + ]); - $this->addItem(array( + $this->addItem([ 'label' => Yii::t('AdminModule.widgets_AdminMenuWidget', 'Advanced'), 'url' => Url::toRoute('/admin/setting/advanced'), 'icon' => '', 'sortOrder' => 1000, 'isVisible' => $canEditSettings - )); + ]); parent::init(); } - } diff --git a/protected/humhub/modules/comment/notifications/NewComment.php b/protected/humhub/modules/comment/notifications/NewComment.php index ea11ca6d21..6044fcedfc 100644 --- a/protected/humhub/modules/comment/notifications/NewComment.php +++ b/protected/humhub/modules/comment/notifications/NewComment.php @@ -38,14 +38,6 @@ class NewComment extends \humhub\modules\notification\components\BaseNotificatio return parent::send($user); } - /** - * @inheritdoc - */ - public static function getTitle() - { - return Yii::t('CommentModule.notifications_NewComment', 'New Comment'); - } - /** * @inheritdoc */ diff --git a/protected/humhub/modules/comment/tests/codeception/fixtures/data/comment.php b/protected/humhub/modules/comment/tests/codeception/fixtures/data/comment.php index e55c3524aa..a1b26e621a 100644 --- a/protected/humhub/modules/comment/tests/codeception/fixtures/data/comment.php +++ b/protected/humhub/modules/comment/tests/codeception/fixtures/data/comment.php @@ -1,9 +1,2 @@ source === null) { Yii::error('Could not render ContentCreated Activity without given source - ' . $this->record->id); return; } - return parent::render($mode, $params); + return parent::render($params); } } diff --git a/protected/humhub/modules/content/components/ContentActiveRecord.php b/protected/humhub/modules/content/components/ContentActiveRecord.php index c485268a85..7474a5f5ef 100644 --- a/protected/humhub/modules/content/components/ContentActiveRecord.php +++ b/protected/humhub/modules/content/components/ContentActiveRecord.php @@ -12,6 +12,7 @@ use Yii; use yii\base\Exception; use humhub\components\ActiveRecord; use humhub\modules\content\models\Content; +use humhub\modules\content\interfaces\ContentOwner; /** * ContentActiveRecord is the base ActiveRecord [[\yii\db\ActiveRecord]] for Content. @@ -41,7 +42,7 @@ use humhub\modules\content\models\Content; * * @author Luke */ -class ContentActiveRecord extends ActiveRecord implements \humhub\modules\content\interfaces\ContentTitlePreview +class ContentActiveRecord extends ActiveRecord implements ContentOwner { /** @@ -188,7 +189,7 @@ class ContentActiveRecord extends ActiveRecord implements \humhub\modules\conten /** * Related Content model * - * @return \yii\db\ActiveQuery + * @return Content */ public function getContent() { diff --git a/protected/humhub/modules/content/components/ContentAddonActiveRecord.php b/protected/humhub/modules/content/components/ContentAddonActiveRecord.php index 393289b186..146e55f1bb 100644 --- a/protected/humhub/modules/content/components/ContentAddonActiveRecord.php +++ b/protected/humhub/modules/content/components/ContentAddonActiveRecord.php @@ -11,6 +11,7 @@ namespace humhub\modules\content\components; use Yii; use yii\base\Exception; use humhub\components\ActiveRecord; +use humhub\modules\content\interfaces\ContentOwner; /** * HActiveRecordContentAddon is the base active record for content addons. @@ -28,7 +29,7 @@ use humhub\components\ActiveRecord; * @package humhub.components * @since 0.5 */ -class ContentAddonActiveRecord extends ActiveRecord implements \humhub\modules\content\interfaces\ContentTitlePreview +class ContentAddonActiveRecord extends ActiveRecord implements ContentOwner { /** diff --git a/protected/humhub/modules/content/components/ContentContainerSettingsManager.php b/protected/humhub/modules/content/components/ContentContainerSettingsManager.php index c30b511a78..9bfab1544e 100644 --- a/protected/humhub/modules/content/components/ContentContainerSettingsManager.php +++ b/protected/humhub/modules/content/components/ContentContainerSettingsManager.php @@ -8,6 +8,7 @@ namespace humhub\modules\content\components; +use Yii; use humhub\libs\BaseSettingsManager; /** @@ -28,6 +29,22 @@ class ContentContainerSettingsManager extends BaseSettingsManager * @var ContentContainerActiveRecord the content container this settings manager belongs to */ public $contentContainer; + + /** + * Returns the setting value of this container for the given setting $name. + * If there is not container specific setting, this function will search for a global setting or + * return default or null if there is also no global setting. + * + * @param type $name + * @param type $default + * @return boolean + * @since 1.2 + */ + public function getInherit($name, $default = null) { + $result = $this->get($name); + return ($result != null) ? $result + : Yii::$app->getModule($this->moduleId)->settings->get($name, $default); + } /** * @inheritdoc diff --git a/protected/humhub/modules/content/interfaces/ContentOwner.php b/protected/humhub/modules/content/interfaces/ContentOwner.php new file mode 100644 index 0000000000..a7871ed716 --- /dev/null +++ b/protected/humhub/modules/content/interfaces/ContentOwner.php @@ -0,0 +1,15 @@ + Html::tag('strong', Html::encode($this->originator->displayName)), diff --git a/protected/humhub/modules/content/tests/codeception/fixtures/ContentContainerFixture.php b/protected/humhub/modules/content/tests/codeception/fixtures/ContentContainerFixture.php index 5a8feed6b1..242170c8e3 100644 --- a/protected/humhub/modules/content/tests/codeception/fixtures/ContentContainerFixture.php +++ b/protected/humhub/modules/content/tests/codeception/fixtures/ContentContainerFixture.php @@ -16,7 +16,8 @@ class ContentContainerFixture extends ActiveFixture public $dataFile = '@modules/content/tests/codeception/fixtures/data/contentcontainer.php'; public $depends = [ - 'humhub\modules\content\tests\codeception\fixtures\ContentContainerPermissionFixture' + 'humhub\modules\content\tests\codeception\fixtures\ContentContainerPermissionFixture', + 'humhub\modules\content\tests\codeception\fixtures\ContentContainerSettingFixture' ]; } diff --git a/protected/humhub/modules/content/tests/codeception/fixtures/ContentContainerSettingFixture.php b/protected/humhub/modules/content/tests/codeception/fixtures/ContentContainerSettingFixture.php new file mode 100644 index 0000000000..254081eebe --- /dev/null +++ b/protected/humhub/modules/content/tests/codeception/fixtures/ContentContainerSettingFixture.php @@ -0,0 +1,19 @@ +id === \humhub\modules\notification\components\MailNotificationTarget::getId()) { + return true; + } else if ($target->id === \humhub\modules\notification\components\WebNotificationTarget::getId()) { + return true; + } else if ($target->id === \humhub\modules\notification\components\MobileNotificationTarget::getId()) { + return true; + } + + return $target->defaultSetting; + } +} diff --git a/protected/humhub/modules/friendship/notifications/Request.php b/protected/humhub/modules/friendship/notifications/Request.php index dbc4671a4b..255617a6cb 100644 --- a/protected/humhub/modules/friendship/notifications/Request.php +++ b/protected/humhub/modules/friendship/notifications/Request.php @@ -37,19 +37,19 @@ class Request extends BaseNotification { return $this->originator->getUrl(); } - + /** * @inheritdoc */ - public static function getTitle() + public function category() { - return Yii::t('FriendshipModule.notifications_Request', 'Friendship Request'); + return new FriendshipNotificationCategory; } /** * @inheritdoc */ - public function getAsHtml() + public function html() { return Yii::t('FriendshipModule.notification', '{displayName} sent you a friend request.', array( 'displayName' => Html::tag('strong', Html::encode($this->originator->displayName)), diff --git a/protected/humhub/modules/friendship/notifications/RequestApproved.php b/protected/humhub/modules/friendship/notifications/RequestApproved.php index 7099ce8a6c..4d67d8c5bd 100644 --- a/protected/humhub/modules/friendship/notifications/RequestApproved.php +++ b/protected/humhub/modules/friendship/notifications/RequestApproved.php @@ -30,6 +30,14 @@ class RequestApproved extends BaseNotification */ public $markAsSeenOnClick = true; + /** + * @inheritdoc + */ + public function category() + { + return new FriendshipNotificationCategory; + } + /** * @inheritdoc */ @@ -41,19 +49,11 @@ class RequestApproved extends BaseNotification /** * @inheritdoc */ - public static function getTitle() + public function html() { - return Yii::t('FriendshipModule.notifications_RequestApproved', 'Friendship Approved'); - } - - /** - * @inheritdoc - */ - public function getAsHtml() - { - return Yii::t('FriendshipModule.notification', '{displayName} accepted your friend request.', array( + return Yii::t('FriendshipModule.notification', '{displayName} accepted your friend request.', [ 'displayName' => Html::tag('strong', Html::encode($this->originator->displayName)), - )); + ]); } } diff --git a/protected/humhub/modules/friendship/notifications/RequestDeclined.php b/protected/humhub/modules/friendship/notifications/RequestDeclined.php index c91bf0c2f6..667f9586a8 100644 --- a/protected/humhub/modules/friendship/notifications/RequestDeclined.php +++ b/protected/humhub/modules/friendship/notifications/RequestDeclined.php @@ -30,6 +30,11 @@ class RequestDeclined extends BaseNotification */ public $markAsSeenOnClick = true; + public function category() + { + return new FriendshipNotificationCategory; + } + /** * @inheritdoc */ diff --git a/protected/humhub/modules/like/notifications/NewLike.php b/protected/humhub/modules/like/notifications/NewLike.php index 9cb21aca07..c4538c7a3a 100644 --- a/protected/humhub/modules/like/notifications/NewLike.php +++ b/protected/humhub/modules/like/notifications/NewLike.php @@ -25,14 +25,6 @@ class NewLike extends BaseNotification */ public $moduleId = 'like'; - /** - * @inheritdoc - */ - public static function getTitle() - { - return Yii::t('LikeModule.notifiations_NewLike', 'New Like'); - } - /** * @inheritdoc */ @@ -45,20 +37,21 @@ class NewLike extends BaseNotification /** * @inheritdoc */ - public function getAsHtml() + public function html() { $contentInfo = $this->getContentInfo($this->getLikedRecord()); if ($this->groupCount > 1) { - return Yii::t('LikeModule.notification', "{displayNames} likes {contentTitle}.", array( + return Yii::t('LikeModule.notification', "{displayNames} likes {contentTitle}.", [ 'displayNames' => $this->getGroupUserDisplayNames(), 'contentTitle' => $contentInfo - )); + ]); } - return Yii::t('LikeModule.notification', "{displayName} likes {contentTitle}.", array( + + return Yii::t('LikeModule.notification', "{displayName} likes {contentTitle}.", [ 'displayName' => Html::tag('strong', Html::encode($this->originator->displayName)), 'contentTitle' => $contentInfo - )); + ]); } /** diff --git a/protected/humhub/modules/like/tests/codeception/fixtures/data/like.php b/protected/humhub/modules/like/tests/codeception/fixtures/data/like.php index e55c3524aa..b993871588 100644 --- a/protected/humhub/modules/like/tests/codeception/fixtures/data/like.php +++ b/protected/humhub/modules/like/tests/codeception/fixtures/data/like.php @@ -1,9 +1,3 @@ from($originator)->about($source)->sendBulk($userList); + * + * This will send Notifications to different NotificationTargets by using a queue. * * @author luke */ @@ -25,34 +30,10 @@ abstract class BaseNotification extends \humhub\components\SocialActivity { /** - * Space this notification belongs to. (Optional) - * If source is a Content, ContentAddon or ContentContainer this will be - * automatically set. - * - * @var \humhub\modules\space\models\Space + * Can be used to delay the NotificationJob execution. + * @var type */ - public $space; - - /** - * Layout file for web version - * - * @var string - */ - protected $layoutWeb = "@notification/views/layouts/web.php"; - - /** - * Layout file for mail version - * - * @var string - */ - protected $layoutMail = "@notification/views/layouts/mail.php"; - - /** - * Layout file for mail plaintext version - * - * @var string - */ - protected $layoutMailPlaintext = "@notification/views/layouts/mail_plaintext.php"; + public $delay = 0; /** * @var boolean automatically mark notification as seen after click on it @@ -69,18 +50,72 @@ abstract class BaseNotification extends \humhub\components\SocialActivity */ protected $_groupKey = null; + /** + * @var \humhub\modules\notification\components\NotificationCategory cached category instance + */ + protected $_category = null; + + /** + * @inheritdoc + */ + public $recordClass = Notification::class; + + /** + * Returns the notification category instance. If no category class is set (default) the default notification settings + * can't be overwritten. + * + * The category instance is cached, once created. + * + * If the Notification configuration should be configurable subclasses have to overwrite this method. + * + * @return \humhub\modules\notification\components\NotificationCategory + */ + public function getCategory() + { + if (!$this->_category) { + $this->_category = $this->category(); + } + return $this->_category; + } + + /** + * Returns a new NotificationCategory instance. + * + * This function should be overwritten by subclasses to append this BaseNotification + * to the returned category. If no category instance is returned, the BaseNotification behavriour (targets) will not be + * configurable. + * + * @return \humhub\modules\notification\components\NotificationCategory + */ + protected function category() + { + return null; + } + + /** + * @return string title text for this notification + */ + public function getTitle() + { + $category = $this->getCategory(); + if ($category) { + return Yii::t('NotificationModule.base', 'There are new updates available at {baseUrl} - ({category})', ['baseUrl' => Url::base(true), 'category' => $category]); + } else { + return Yii::t('NotificationModule.base', 'There are new updates available at {baseUrl}', ['baseUrl' => Url::base(true)]); + } + } + /** * @inheritdoc */ public function getViewParams($params = []) { - $params['url'] = Url::to(['/notification/entry', 'id' => $this->record->id], true); - $params['space'] = $this->space; - $params['isNew'] = ($this->record->seen != 1); - $params['asHtml'] = $this->getAsHtml(); - $params['asText'] = $this->getAsText(); + $result = [ + 'url' => Url::to(['/notification/entry', 'id' => $this->record->id], true), + 'isNew' => !$this->record->seen, + ]; - return parent::getViewParams($params); + return \yii\helpers\ArrayHelper::merge(parent::getViewParams($result), $params); } /** @@ -100,57 +135,98 @@ abstract class BaseNotification extends \humhub\components\SocialActivity } /** - * Sends this notification to a User + * Sends this notification to all notification targets of this User * * @param User $user */ public function send(User $user) { - - if ($this->moduleId == "") { - throw new \yii\base\InvalidConfigException("No moduleId given!"); + if (empty($this->moduleId)) { + throw new \yii\base\InvalidConfigException('No moduleId given for "' . $this->className() . '"'); } // Skip - do not set notification to the originator - if ($this->originator !== null && $user->id == $this->originator->id) { + if ($this->originator && $this->originator->id == $user->id) { return; } - - $notification = new Notification; - $notification->user_id = $user->id; - $notification->class = $this->className(); - $notification->module = $this->moduleId; - $notification->seen = 0; - - // Load group key - if ($this->_groupKey === null) { - $this->_groupKey = $this->getGroupKey(); + + //$this->queueJob($user); + $this->saveRecord($user); + foreach (Yii::$app->notification->getTargets($user) as $target) { + $target->send($this, $user); } + } - if ($this->_groupKey !== '') { - $notification->group_key = $this->getGroupKey(); - } + /** + * Queues the notification job. The Job is responsible for creating and sending + * the Notifications out to its NotificationTargets. + * + * @param User $user + */ + protected function queueJob(User $user) + { + Yii::$app->notificationQueue->push(new NotificationJob([ + 'notification' => $this, + 'user_id' => $user->id + ])); + } - if ($this->source !== null) { + public function save(User $user) + { + // We reuse our record instance to save multiple records. + $this->record->id = null; + $this->record->isNewRecord = true; + $this->record->user_id = $user->id; + return $this->record->save(); + } + /** + * Creates the an Notification instance of the current BaseNotification type for the + * given $user. + * + * @param User $user + */ + public function saveRecord(User $user) + { + $notification = new Notification([ + 'user_id' => $user->id, + 'class' => static::class, + 'module' => $this->moduleId, + 'group_key' => $this->getGroupKey(), + ]); + + if ($this->source) { $notification->source_pk = $this->source->getPrimaryKey(); $notification->source_class = $this->source->className(); - - // Automatically set spaceId if source is Content/Addon/Container - if ($this->source instanceof ContentActiveRecord || $this->source instanceof ContentAddonActiveRecord) { - if ($this->source->content->container instanceof \humhub\modules\space\models\Space) { - $notification->space_id = $this->source->content->container->id; - } - } elseif ($this->source instanceof \humhub\modules\space\models\Space) { - $notification->space_id = $this->source->id; - } + $notification->space_id = $this->getSpaceId(); } - if ($this->originator !== null) { + if ($this->originator) { $notification->originator_user_id = $this->originator->id; } $notification->save(); + $this->record = $notification; + } + + /** + * @inheritdoc + */ + public function about($source) + { + parent::about($source); + $this->record->space_id = $this->getSpaceId(); + return $this; + } + + /** + * @inheritdoc + */ + public function from($originator) + { + $this->originator = $originator; + $this->record->originator_user_id = $originator->id; + return $this; } /** @@ -160,7 +236,7 @@ abstract class BaseNotification extends \humhub\components\SocialActivity { $condition = []; - $condition['class'] = $this->className(); + $condition['class'] = static::class; if ($user !== null) { $condition['user_id'] = $user->id; @@ -201,71 +277,38 @@ abstract class BaseNotification extends \humhub\components\SocialActivity $similarNotifications = Notification::find() ->where(['source_class' => $this->record->source_class, 'source_pk' => $this->record->source_pk, 'user_id' => $this->record->user_id]) ->andWhere(['!=', 'seen', '1']); - foreach ($similarNotifications->all() as $n) { - $n->getClass()->markAsSeen(); + foreach ($similarNotifications->all() as $notification) { + $notification->getClass()->markAsSeen(); } } /** - * Returns key to group notifications. - * If empty notification grouping is disabled. + * Returns a key for grouping notifications. + * If null is returned (default) the notification grouping for this BaseNotification type disabled. + * + * The returned key could for example be a combination of classname related content id. * * @return string the group key */ public function getGroupKey() - { - return ""; - } - - /** - * Should be overwritten by subclasses. This method provides a user friendly - * title for the different notification types. - * - * @return string e.g. New Like - */ - public static function getTitle() { return null; } /** - * Returns text version of this notification + * Renders the Notificaiton for the given NotificationTarget. + * Subclasses are able to use custom renderer for different targets by overwriting this function. * - * @return string + * @param NotificationTarger $target + * @return string render result */ - public function getAsText() + public function render(NotificationTarget $target = null) { - $html = $this->getAsHtml(); - - if ($html === null) { - return null; + if (!$target) { + $target = Yii::$app->notification->getTarget(WebNotificationTarget::class); } - return strip_tags($html); - } - - /** - * Returns Html text of this notification, - * - * @return type - */ - public function getAsHtml() - { - return null; - } - - /** - * @inheritdoc - */ - public function render($mode = self::OUTPUT_WEB, $params = array()) - { - // Set default notification view - when not specified - if ($this->viewName === null) { - $this->viewName = 'default'; - $this->viewPath = '@notification/views/notification'; - } - - return parent::render($mode, $params); + return $target->getRenderer()->render($this); } /** @@ -322,4 +365,42 @@ abstract class BaseNotification extends \humhub\components\SocialActivity return $users; } + /** + * @inheritdoc + */ + public function asArray() + { + $result = parent::asArray(); + $result['title'] = $this->getTitle(); + return $result; + } + + /** + * Should be overwritten by subclasses for a html representation of the notification. + * @return type + */ + public function html() + { + // Only for backward compatibility. + return $this->getAsHtml(); + } + + /** + * Use text() instead + * @deprecated since version 1.2 + */ + public function getAsText() + { + return $this->text(); + } + + /** + * Use html() instead + * @deprecated since version 1.2 + */ + public function getAsHtml() + { + return null; + } + } diff --git a/protected/humhub/modules/notification/components/MailNotificationTarget.php b/protected/humhub/modules/notification/components/MailNotificationTarget.php new file mode 100644 index 0000000000..1524041771 --- /dev/null +++ b/protected/humhub/modules/notification/components/MailNotificationTarget.php @@ -0,0 +1,76 @@ + '@humhub/modules/content/views/mails/Update', + 'text' => '@humhub/modules/content/views/mails/plaintext/Update' + ]; + + /** + * @inheritdoc + */ + public function getTitle() + { + return Yii::t('NotificationModule.components_WebNotificationTarget', 'E-Mail'); + } + + /** + * @inheritdoc + */ + public function handle(BaseNotification $notification, User $user) + { + try { + // TODO: find cleaner solution... + Yii::$app->view->params['showUnsubscribe'] = true; + + $viewParams = [ + 'notifications' => $notification->render($this), + 'notifications_plaintext' => $this->getText($notification) + ]; + + return Yii::$app->mailer->compose($this->view, $viewParams) + ->setFrom([Yii::$app->settings->get('mailer.systemEmailAddress') => Yii::$app->settings->get('mailer.systemEmailName')]) + ->setTo($user->email) + ->setSubject($notification->getTitle())->send(); + } catch (\Exception $ex) { + Yii::error('Could not send mail to: ' . $user->email . ' - Error: ' . $ex->getMessage()); + if(YII_DEBUG) { + throw $ex; + } + } + } + + public function getText(BaseNotification $notification) + { + $textRenderer = $this->getRenderer(); + + if (!method_exists($textRenderer, 'renderText')) { + $textRenderer = Yii::createObject(MailTargetRenderer::class); + } + + return $textRenderer->renderText($notification); + } + +} diff --git a/protected/humhub/modules/notification/components/MailTargetRenderer.php b/protected/humhub/modules/notification/components/MailTargetRenderer.php new file mode 100644 index 0000000000..a882a71a5b --- /dev/null +++ b/protected/humhub/modules/notification/components/MailTargetRenderer.php @@ -0,0 +1,125 @@ +getViewPath($viewable) . '/notification/mail/' . $viewable->getViewName(); + + if (!file_exists($viewFile)) { + $viewFile = Yii::getAlias($this->defaultViewPath) . DIRECTORY_SEPARATOR . $viewable->getViewName(); + } + + if (!file_exists($viewFile)) { + $viewFile = Yii::getAlias($this->defaultView); + } + + return $viewFile; + } + + /** + * Returns the layout for the given Notification Viewable. + * + * This function will search for a layout file under module/views/layouts/mail with the view name defined + * by $viwable. + * + * If this file does not exists the default notification mail layout will be returned. + * + * @param \humhub\modules\notification\components\Viewable $viewable + * @return type + */ + public function getLayout(Viewable $viewable) + { + $layout = $this->getViewPath($viewable) . '/layouts/notification/mail/' . $viewable->getViewName(); + + if (!file_exists($layout)) { + $layout = Yii::getAlias($this->defaultLayout); + } + + return $layout; + } + + /** + * Returns the text layout for the given Notification Viewable. + * + * This function will search for a view file under module/views/layouts/mail/plaintext with the view name defined + * by $viwable. + * + * If this file does not exists the default notification text mail layout is returned. + * + * @param \humhub\modules\notification\components\Viewable $viewable + * @return type + */ + public function getTextLayout(Viewable $viewable) + { + $layout = $this->getViewPath($viewable) . '/layouts/notification/mail/plaintext/' . $viewable->getViewName(); + + if (!file_exists($layout)) { + $layout = Yii::getAlias($this->defaultTextLayout); + } + + return $layout; + } + +} diff --git a/protected/humhub/modules/notification/components/MobileNotificationTarget.php b/protected/humhub/modules/notification/components/MobileNotificationTarget.php new file mode 100644 index 0000000000..e370a84b82 --- /dev/null +++ b/protected/humhub/modules/notification/components/MobileNotificationTarget.php @@ -0,0 +1,33 @@ +id) { + throw new \yii\base\InvalidConfigException('NotificationCategories have to define an id property, which is not the case for "'.self::class.'"'); + } + } + + /** + * Returns a human readable title of this category + */ + public abstract function getTitle(); + + /** + * Returns a group description + */ + public abstract function getDescription(); + + /** + * Returns the default enabled settings for the given $target. + * In case the $target is unknown, subclasses can either return $target->defaultSetting + * or another default value. + * + * @param NotificationTarget $target + * @return boolean + */ + public function getDefaultSetting(NotificationTarget $target) + { + if ($target->id === \humhub\modules\notification\components\MailNotificationTarget::getId()) { + return true; + } else if ($target->id === \humhub\modules\notification\components\WebNotificationTarget::getId()) { + return true; + } else if ($target->id === \humhub\modules\notification\components\MobileNotificationTarget::getId()) { + return false; + } + + return $target->defaultSetting; + } + + /** + * Returns an array of target ids, which are not editable. + * + * @param NotificationTarget $target + */ + public function getFixedSettings() + { + return []; + } + + /** + * Checks if the given NotificationTarget is fixed for this category. + * + * @param type $target + * @return type + */ + public function isFixedSettings(NotificationTarget $target) + { + return in_array($target->id, $this->getFixedSettings()); + } + + /** + * Determines if this category is visible for the given $user. + * This can be used if a category is only visible for users with certian permissions. + * + * Note if no user is given this function should return true in most cases, otherwise this + * category won't be visible in the global notification settings. + * + * @param User $user + * @return boolean + */ + public function isVisible(User $user = null) + { + return true; + } +} diff --git a/protected/humhub/modules/notification/components/NotificationManager.php b/protected/humhub/modules/notification/components/NotificationManager.php new file mode 100644 index 0000000000..7cd5012fc1 --- /dev/null +++ b/protected/humhub/modules/notification/components/NotificationManager.php @@ -0,0 +1,133 @@ +_targets) { + return $this->_targets; + } + + foreach($this->targets as $target) { + $instance = Yii::createObject($target); + if($instance->isActive($user)) { + $this->_targets[] = $instance; + } + } + + return $this->_targets; + } + + public function getTarget($class) + { + foreach($this->getTargets() as $target) { + if($target->className() == $class) { + return $target; + } + } + } + + /** + * Returns all available Notifications + * + * @return type + */ + public function getNotifications() + { + if (!$this->_notifications) { + $this->_notifications = $this->searchModuleNotifications(); + } + return $this->_notifications; + } + + /** + * Returns all available NotificationCategories as array with category id as + * key and a category instance as value. + */ + public function getNotificationCategories($user = null) + { + if($this->_categories) { + return $this->_categories; + } + + $result = []; + + foreach($this->getNotifications() as $notification) { + $category = $notification->getCategory(); + if($category && !array_key_exists($category->id, $result) && $category->isVisible($user)) { + $result[$category->id] = $category; + } + } + + $this->_categories = array_values($result); + + usort($this->_categories, function($a, $b) { + return $a->sortOrder - $b->sortOrder; + }); + + return $this->_categories; + } + + /** + * Searches for all Notifications exported by modules. + * @return type + */ + protected function searchModuleNotifications() + { + $result = []; + foreach (Yii::$app->moduleManager->getModules(['includeCoreModules' => true]) as $module) { + if ($module instanceof \humhub\components\Module && $module->hasNotifications()) { + $result = array_merge($result, $this->createNotifications($module->getNotifications())); + } + } + return $result; + } + + protected function createNotifications($notificationClasses) + { + $result = []; + foreach($notificationClasses as $notificationClass) { + $result[] = Yii::createObject($notificationClass); + } + return $result; + } + +} diff --git a/protected/humhub/modules/notification/components/NotificationTarget.php b/protected/humhub/modules/notification/components/NotificationTarget.php new file mode 100644 index 0000000000..bcba27106b --- /dev/null +++ b/protected/humhub/modules/notification/components/NotificationTarget.php @@ -0,0 +1,242 @@ +title = $this->getTitle(); + } + + /** + * @return string Human readable title for views. + */ + public abstract function getTitle(); + + /** + * @return \humhub\components\rendering\Renderer default renderer for this target. + */ + public function getRenderer() + { + return \yii\di\Instance::ensure($this->renderer, Renderer::class); + } + + /** + * Used to handle a BaseNotification for a given $user. + * + * The NotificationTarget can handle the notification for example by pushing a Job to + * a Queue or directly handling the notification. + * + * @param BaseNotification $notification + */ + public abstract function handle(BaseNotification $notification, User $user); + + /** + * Used to acknowledge the seding/processing of the given $notification. + * + * @param BaseNotification $notification notification to be acknowledged + * @param boolean $state true if successful otherwise false + */ + public function acknowledge(BaseNotification $notification, $state = true) + { + if ($this->acknowledgeFlag && $notification->record->hasAttribute($this->acknowledgeFlag)) { + $notification->record->setAttribute($this->acknowledgeFlag, $state); + $notification->record->save(); + } + } + + /** + * @return boolean Check if the given $notification has already been processed. + */ + public function isAcknowledged(BaseNotification $notification) + { + if ($this->acknowledgeFlag && $notification->record->hasAttribute($this->acknowledgeFlag)) { + return $notification->record->getAttribute($this->acknowledgeFlag); + } + return false; + } + + /** + * Static access to the target id. + * + * @return string + */ + public static function getId() + { + $instance = new static(); + return $instance->id; + } + + /** + * Used to process a $notification for the given $user. + * + * By default the $noification will be marked as acknowledged before processing. + * The processing is triggerd by calling $this->handle. + * If the processing fails the acknowledged mark will be set to false. + * + * @param BaseNotification $notification + */ + public function send(BaseNotification $notification, User $user) + { + if ($this->isAcknowledged($notification)) { + return; + } + + try { + $this->acknowledge($notification, true); + + if ($this->isEnabled($notification, $user)) { + $this->handle($notification, $user); + } else { + $this->acknowledge($notification, false); + } + } catch (\Exception $e) { + Yii::error($e); + $this->acknowledge($notification, false); + //TODO: increment retry count. + } + } + + /** + * Used for handling the given $notification for multiple $users. + * + * @param BaseNotification $notification + * @param type $users + */ + public function sendBulk(BaseNotification $notification, $users) + { + if ($users instanceof \yii\db\ActiveQuery) { + $users = $users->all(); + } + + foreach ($users as $user) { + $this->send($notification, $user); + } + } + + /** + * Returns the setting key for this target of the given $category. + * @param type $category + * @return type + */ + public function getSettingKey($category) + { + return $category->id . '_' . $this->id; + } + + /** + * Some NotificationTargets may need to be activated first or require a certain permission in order to be used. + * + * This function checks if this target is active for the given user. + * If no user is given this function will determine if the target is globaly active or deactivated. + * + * If a subclass does not overwrite this function it will be activated for all users by default. + * + * @param User $user + */ + public function isActive(User $user = null) + { + return true; + } + + /** + * Checks if the given $notification is enabled for this target. + * If the $notification is not part of a NotificationCategory the $defaultSetting + * of this NotificationTarget is returned. + * + * If this NotificationTarget is not active for the given $user, this function will return false. + * + * @param BaseNotification $notification + * @param User $user + * @see NotificationTarget::isActive() + * @see NotificationTarget::isCategoryEnabled() + * @return boolean + */ + public function isEnabled(BaseNotification $notification, User $user = null) + { + if (!$this->isActive($user)) { + return false; + } + + $category = $notification->getCategory(); + return ($category) ? $this->isCategoryEnabled($category, $user) : $this->defaultSetting; + } + + /** + * Returns the enabled setting of this target for the given $category. + * + * @param NotificationCategory $category + * @param User $user + * @return boolean + */ + public function isCategoryEnabled(NotificationCategory $category, User $user = null) + { + if(!$category->isVisible($user)) { + return false; + } + + if($category->isFixedSettings($this)) { + return $category->getDefaultSetting($this); + } + + $settingKey = $this->getSettingKey($category); + + if ($user) { + $enabled = Yii::$app->getModule('notification')->settings->user($user)->getInherit($settingKey, $category->getDefaultSetting($this)); + } else { + $enabled = Yii::$app->getModule('notification')->settings->get($settingKey, $category->getDefaultSetting($this)); + } + + return ($enabled === null) ? $this->defaultSetting : boolval($enabled); + } + +} diff --git a/protected/humhub/modules/notification/components/WebNotificationTarget.php b/protected/humhub/modules/notification/components/WebNotificationTarget.php new file mode 100644 index 0000000000..7e1fd7374c --- /dev/null +++ b/protected/humhub/modules/notification/components/WebNotificationTarget.php @@ -0,0 +1,32 @@ +record) { + throw new \yii\base\Exception('Notification record not found for BaseNotification "'.$notification->className().'"'); + } + } + + public function getTitle() + { + return Yii::t('NotificationModule.components_WebNotificationTarget', 'Web'); + } +} diff --git a/protected/humhub/modules/notification/components/WebTargetRenderer.php b/protected/humhub/modules/notification/components/WebTargetRenderer.php new file mode 100644 index 0000000000..35a326bbfd --- /dev/null +++ b/protected/humhub/modules/notification/components/WebTargetRenderer.php @@ -0,0 +1,95 @@ +getViewPath($viewable) . '/'.$this->viewSubFolder.'/' . $viewable->getViewName(); + + if (!file_exists($viewFile)) { + $viewFile = Yii::getAlias($this->defaultViewPath) . DIRECTORY_SEPARATOR . $viewable->getViewName(); + } + + if (!file_exists($viewFile)) { + $viewFile = Yii::getAlias($this->defaultView); + } + + return $viewFile; + } + + /** + * Returns the layout for the given Notification Viewable. + * + * This function will search for a layout file under module/views/layouts/mail with the view name defined + * by $viwable. + * + * If this file does not exists the default notification mail layout will be returned. + * + * @param \humhub\modules\notification\components\Viewable $viewable + * @return type + */ + public function getLayout(Viewable $viewable) + { + $layout = $this->getViewPath($viewable) . '/layouts/'.$this->viewSubFolder.'/' . $viewable->getViewName(); + + if (!file_exists($layout)) { + $layout = Yii::getAlias($this->defaultLayout); + } + + return $layout; + } + +} diff --git a/protected/humhub/modules/notification/models/Notification.php b/protected/humhub/modules/notification/models/Notification.php index 4a84b17bbd..e64b700bdf 100644 --- a/protected/humhub/modules/notification/models/Notification.php +++ b/protected/humhub/modules/notification/models/Notification.php @@ -27,6 +27,14 @@ class Notification extends \humhub\components\ActiveRecord */ public $group_count; + public function init() + { + parent::init(); + if ($this->seen == null) { + $this->seen = 0; + } + } + /** * @inheritdoc */ @@ -46,13 +54,23 @@ class Notification extends \humhub\components\ActiveRecord [['class', 'source_class'], 'string', 'max' => 100] ]; } + + /** + * Use getBaseModel instead. + * @deprecated since version 1.2 + * @param type $params + */ + public function getClass($params = []) + { + return $this->getBaseModel($params); + } /** * Returns the business model of this notification * * @return \humhub\modules\notification\components\BaseNotification */ - public function getClass($params = []) + public function getBaseModel($params = []) { if (class_exists($this->class)) { $params['source'] = $this->getSourceObject(); @@ -155,7 +173,7 @@ class Notification extends \humhub\components\ActiveRecord public static function findGrouped() { $query = self::find(); - $query->addSelect(['notification.*', + $query->addSelect(['notification.*', new \yii\db\Expression('count(distinct(originator_user_id)) as group_count'), new \yii\db\Expression('max(created_at) as group_created_at'), new \yii\db\Expression('min(seen) as group_seen'), diff --git a/protected/humhub/modules/notification/models/forms/NotificationSettings.php b/protected/humhub/modules/notification/models/forms/NotificationSettings.php new file mode 100644 index 0000000000..1efe95c578 --- /dev/null +++ b/protected/humhub/modules/notification/models/forms/NotificationSettings.php @@ -0,0 +1,53 @@ +_targets) { + $this->_targets = Yii::$app->notification->getTargets($user); + } + + return $this->_targets; + } + + public function categories($user = null) + { + return Yii::$app->notification->getNotificationCategories($user); + } + + public function getSettingFormname($category, $target) + { + return $this->formName()."[settings][".$target->getSettingKey($category)."]"; + } + + public function save($user = null) + { + $module = Yii::$app->getModule('notification'); + $settingManager = ($user) ? $module->settings->user($user) : $module->settings; + foreach ($this->settings as $settingKey => $value) { + $settingManager->set($settingKey, $value); + } + } + +} diff --git a/protected/humhub/modules/notification/tests/codeception/_support/AcceptanceTester.php b/protected/humhub/modules/notification/tests/codeception/_support/AcceptanceTester.php index df62ef9fd8..2312d7dcf4 100644 --- a/protected/humhub/modules/notification/tests/codeception/_support/AcceptanceTester.php +++ b/protected/humhub/modules/notification/tests/codeception/_support/AcceptanceTester.php @@ -1,5 +1,5 @@ getCategory(); + $mailTarget = Yii::$app->notification->getTarget(MailNotificationTarget::class); + $webTarget = Yii::$app->notification->getTarget(WebNotificationTarget::class); + + $this->assertFalse($mailTarget->isEnabled($notification)); + $this->assertTrue($webTarget->isEnabled($notification)); + + $settingForm = new NotificationSettings([ + 'settings' => [ + $mailTarget->getSettingKey($category) => true, + $webTarget->getSettingKey($category) => false, + ] + ]); + + $settingForm->save(); + + $this->assertTrue($mailTarget->isEnabled($notification)); + $this->assertFalse($webTarget->isEnabled($notification)); + } + + public function testFixedCategorySetting() + { + $notification = new SpecialNotification(); + $category = $notification->getCategory(); + $mailTarget = Yii::$app->notification->getTarget(MailNotificationTarget::class); + $webTarget = Yii::$app->notification->getTarget(WebNotificationTarget::class); + + $this->assertFalse($mailTarget->isEnabled($notification)); + $this->assertFalse($webTarget->isEnabled($notification)); + + // Set true for both + $settingForm = new NotificationSettings([ + 'settings' => [ + $mailTarget->getSettingKey($category) => true, + $webTarget->getSettingKey($category) => true, + ] + ]); + + $settingForm->save(); + + // Check that setting does not effect fixed target setting. + $this->assertTrue($webTarget->isEnabled($notification)); + $this->assertFalse($mailTarget->isEnabled($notification)); + } + + public function testInvisibleCategorySetting() + { + // SpecialCategory is invisible for this user. + $this->becomeUser('User1'); + $user = Yii::$app->user->getIdentity(); + $notification = new SpecialNotification(); + $category = $notification->getCategory(); + $mailTarget = Yii::$app->notification->getTarget(MailNotificationTarget::class); + $webTarget = Yii::$app->notification->getTarget(WebNotificationTarget::class); + + $this->assertFalse($mailTarget->isEnabled($notification)); + $this->assertFalse($webTarget->isEnabled($notification)); + + // Set global settings to true for both targets + $settingForm = new NotificationSettings([ + 'settings' => [ + $mailTarget->getSettingKey($category) => true, + $webTarget->getSettingKey($category) => true, + ] + ]); + + $settingForm->save(); + + // Check this does not affect the decision for this user + $this->assertFalse($webTarget->isEnabled($notification, $user)); + $this->assertFalse($mailTarget->isEnabled($notification, $user)); + + // Save again for the user + $settingForm = new NotificationSettings([ + 'settings' => [ + $mailTarget->getSettingKey($category) => true, + $webTarget->getSettingKey($category) => true, + ] + ]); + + $settingForm->save($user); + + // Check this does not affect the decision for this user + $this->assertFalse($webTarget->isEnabled($notification, $user)); + $this->assertFalse($mailTarget->isEnabled($notification, $user)); + } + + public function testUserCategorySetting() + { + $this->becomeUser('User2'); + $user = Yii::$app->user->getIdentity(); + $notification = new TestNotification(); + $category = $notification->getCategory(); + $mailTarget = Yii::$app->notification->getTarget(MailNotificationTarget::class); + $webTarget = Yii::$app->notification->getTarget(WebNotificationTarget::class); + + // Check default settings. + $this->assertFalse($mailTarget->isEnabled($notification, $user)); + $this->assertTrue($webTarget->isEnabled($notification, $user)); + + // Change global default settings, deny both targets. + $settingForm = new NotificationSettings([ + 'settings' => [ + $mailTarget->getSettingKey($category) => false, + $webTarget->getSettingKey($category) => false, + ] + ]); + + $settingForm->save(); + + // Check if global defaults effected user check + $this->assertFalse($mailTarget->isEnabled($notification, $user)); + $this->assertFalse($webTarget->isEnabled($notification, $user)); + + // Change user settings. + $userSettings = new NotificationSettings([ + 'settings' => [ + $mailTarget->getSettingKey($category) => true, + $webTarget->getSettingKey($category) => true, + ] + ]); + + $userSettings->save($user); + + // Check that global settings are unaffected + $this->assertFalse($mailTarget->isEnabled($notification)); + $this->assertFalse($webTarget->isEnabled($notification)); + + // Check if user settings + $this->assertTrue($mailTarget->isEnabled($notification, $user)); + $this->assertTrue($webTarget->isEnabled($notification, $user)); + } +} diff --git a/protected/humhub/modules/notification/tests/codeception/unit/category/notifications/SpecialNotification.php b/protected/humhub/modules/notification/tests/codeception/unit/category/notifications/SpecialNotification.php new file mode 100644 index 0000000000..55dff0f23e --- /dev/null +++ b/protected/humhub/modules/notification/tests/codeception/unit/category/notifications/SpecialNotification.php @@ -0,0 +1,20 @@ +id === \humhub\modules\notification\components\MailNotificationTarget::getId()) { + return false; + } else if ($target->id === \humhub\modules\notification\components\WebNotificationTarget::getId()) { + return false; + } + + return $target->defaultSetting; + } + + public function getFixedSettings() + { + return [\humhub\modules\notification\components\MailNotificationTarget::getId()]; + } + + public function isVisible(\humhub\modules\user\models\User $user = null) + { + return !$user || $user->id != 2; + } + + public function getDescription() + { + return 'My Special Test Notification Category'; + } + + public function getTitle() + { + return 'Test Special Category'; + } + +} diff --git a/protected/humhub/modules/notification/tests/codeception/unit/category/notifications/TestNotification.php b/protected/humhub/modules/notification/tests/codeception/unit/category/notifications/TestNotification.php new file mode 100644 index 0000000000..975e3247e0 --- /dev/null +++ b/protected/humhub/modules/notification/tests/codeception/unit/category/notifications/TestNotification.php @@ -0,0 +1,19 @@ +id === \humhub\modules\notification\components\MailNotificationTarget::getId()) { + return false; + } else if ($target->id === \humhub\modules\notification\components\WebNotificationTarget::getId()) { + return true; + } + + return $target->defaultSetting; + } + + + public function getDescription() + { + return 'My Test Notification Category'; + } + + public function getTitle() + { + return 'Test Category'; + } + +} diff --git a/protected/humhub/modules/notification/tests/codeception/unit/rendering/MailTargetRenderTest.php b/protected/humhub/modules/notification/tests/codeception/unit/rendering/MailTargetRenderTest.php new file mode 100644 index 0000000000..047b29566e --- /dev/null +++ b/protected/humhub/modules/notification/tests/codeception/unit/rendering/MailTargetRenderTest.php @@ -0,0 +1,43 @@ +notification->getTarget(MailNotificationTarget::class); + $renderer = $target->getRenderer(); + $this->assertContains('

TestedMailViewNotificationHTML

', $renderer->render($notification)); + $this->assertContains('TestedMailViewNotificationText', $renderer->renderText($notification)); + } + + public function testOverwriteViewFile() + { + $notification = notifications\TestedMailViewNotification::instance(); + $notification->viewName = 'special'; + $target = Yii::$app->notification->getTarget(MailNotificationTarget::class); + $renderer = $target->getRenderer(); + $this->assertContains('
Special:

TestedMailViewNotificationHTML

', $renderer->render($notification)); + $this->assertContains('TestedMailViewNotificationText', $renderer->renderText($notification)); + } + + public function testOverwriteLayoutFile() + { + $notification = notifications\TestedMailViewNotification::instance(); + $notification->viewName = 'specialLayout'; + $target = Yii::$app->notification->getTarget(MailNotificationTarget::class); + $renderer = $target->getRenderer(); + $this->assertEquals('
MyLayout:

TestedMailViewNotificationHTML

', trim($renderer->render($notification))); + $this->assertEquals('MyLayout:TestedMailViewNotificationText', trim($renderer->renderText($notification))); + } +} diff --git a/protected/humhub/modules/notification/tests/codeception/unit/rendering/WebTargetRenderTest.php b/protected/humhub/modules/notification/tests/codeception/unit/rendering/WebTargetRenderTest.php new file mode 100644 index 0000000000..5d5de98f66 --- /dev/null +++ b/protected/humhub/modules/notification/tests/codeception/unit/rendering/WebTargetRenderTest.php @@ -0,0 +1,46 @@ +notification->getTarget(WebNotificationTarget::class); + $renderer = $target->getRenderer(); + $result = $renderer->render($notification); + $this->assertContains('New', $result); + $this->assertContains('

TestedMailViewNotificationHTML

', $result); + } + + public function testOverwriteViewFile() + { + $notification = notifications\TestedMailViewNotification::instance(); + $notification->viewName = 'special'; + $target = Yii::$app->notification->getTarget(WebNotificationTarget::class); + $renderer = $target->getRenderer(); + $result = $renderer->render($notification); + $this->assertContains('New', $result); + $this->assertContains('
Special:

TestedMailViewNotificationHTML

', $result); + } + + public function testOverwriteLayoutFile() + { + $notification = notifications\TestedMailViewNotification::instance(); + $notification->viewName = 'specialLayout'; + $target = Yii::$app->notification->getTarget(WebNotificationTarget::class); + $renderer = $target->getRenderer(); + $result = $renderer->render($notification); + $this->assertNotContains('New', $result); + $this->assertEquals('
MyLayout:

TestedMailViewNotificationHTML

', trim($result)); + } +} diff --git a/protected/humhub/modules/notification/tests/codeception/unit/rendering/notifications/TestedMailViewNotification.php b/protected/humhub/modules/notification/tests/codeception/unit/rendering/notifications/TestedMailViewNotification.php new file mode 100644 index 0000000000..64218a56d3 --- /dev/null +++ b/protected/humhub/modules/notification/tests/codeception/unit/rendering/notifications/TestedMailViewNotification.php @@ -0,0 +1,21 @@ +TestedMailViewNotificationHTML'; + } + + public function text() + { + return 'TestedMailViewNotificationText'; + } +} diff --git a/protected/humhub/modules/notification/tests/codeception/unit/rendering/views/layouts/notification/mail/plaintext/specialLayout.php b/protected/humhub/modules/notification/tests/codeception/unit/rendering/views/layouts/notification/mail/plaintext/specialLayout.php new file mode 100644 index 0000000000..d6a2ecf717 --- /dev/null +++ b/protected/humhub/modules/notification/tests/codeception/unit/rendering/views/layouts/notification/mail/plaintext/specialLayout.php @@ -0,0 +1,2 @@ +MyLayout: + diff --git a/protected/humhub/modules/notification/tests/codeception/unit/rendering/views/layouts/notification/mail/specialLayout.php b/protected/humhub/modules/notification/tests/codeception/unit/rendering/views/layouts/notification/mail/specialLayout.php new file mode 100644 index 0000000000..cf088e5993 --- /dev/null +++ b/protected/humhub/modules/notification/tests/codeception/unit/rendering/views/layouts/notification/mail/specialLayout.php @@ -0,0 +1,2 @@ +
MyLayout:
+ diff --git a/protected/humhub/modules/notification/tests/codeception/unit/rendering/views/layouts/notification/specialLayout.php b/protected/humhub/modules/notification/tests/codeception/unit/rendering/views/layouts/notification/specialLayout.php new file mode 100644 index 0000000000..cf088e5993 --- /dev/null +++ b/protected/humhub/modules/notification/tests/codeception/unit/rendering/views/layouts/notification/specialLayout.php @@ -0,0 +1,2 @@ +
MyLayout:
+ diff --git a/protected/humhub/modules/notification/tests/codeception/unit/rendering/views/notification/mail/special.php b/protected/humhub/modules/notification/tests/codeception/unit/rendering/views/notification/mail/special.php new file mode 100644 index 0000000000..645cf5c65c --- /dev/null +++ b/protected/humhub/modules/notification/tests/codeception/unit/rendering/views/notification/mail/special.php @@ -0,0 +1,2 @@ +
Special:
+ diff --git a/protected/humhub/modules/notification/tests/codeception/unit/rendering/views/notification/special.php b/protected/humhub/modules/notification/tests/codeception/unit/rendering/views/notification/special.php new file mode 100644 index 0000000000..645cf5c65c --- /dev/null +++ b/protected/humhub/modules/notification/tests/codeception/unit/rendering/views/notification/special.php @@ -0,0 +1,2 @@ +
Special:
+ diff --git a/protected/humhub/modules/notification/views/layouts/mail.php b/protected/humhub/modules/notification/views/layouts/mail.php index 05e18499d7..a20ad7ff60 100644 --- a/protected/humhub/modules/notification/views/layouts/mail.php +++ b/protected/humhub/modules/notification/views/layouts/mail.php @@ -64,7 +64,7 @@ use yii\helpers\Html; - + diff --git a/protected/humhub/modules/notification/views/layouts/web.php b/protected/humhub/modules/notification/views/layouts/web.php index a406babfbf..6913b0dc4e 100644 --- a/protected/humhub/modules/notification/views/layouts/web.php +++ b/protected/humhub/modules/notification/views/layouts/web.php @@ -8,7 +8,7 @@ ?>
  • - +
    @@ -30,10 +30,10 @@
    - +
    $record->created_at]); ?> - +
    diff --git a/protected/humhub/modules/notification/views/notification/default.php b/protected/humhub/modules/notification/views/notification/default.php index 25f0d5f5a9..1c69925a76 100644 --- a/protected/humhub/modules/notification/views/notification/default.php +++ b/protected/humhub/modules/notification/views/notification/default.php @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/protected/humhub/modules/notification/views/notification/mail/default.php b/protected/humhub/modules/notification/views/notification/mail/default.php new file mode 100644 index 0000000000..1c69925a76 --- /dev/null +++ b/protected/humhub/modules/notification/views/notification/mail/default.php @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/protected/humhub/modules/notification/views/notification/mail/plaintext/default.php b/protected/humhub/modules/notification/views/notification/mail/plaintext/default.php index b6a08c1fb3..039a97d3d5 100644 --- a/protected/humhub/modules/notification/views/notification/mail/plaintext/default.php +++ b/protected/humhub/modules/notification/views/notification/mail/plaintext/default.php @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/protected/humhub/modules/notification/widgets/NotificationSettingsForm.php b/protected/humhub/modules/notification/widgets/NotificationSettingsForm.php new file mode 100644 index 0000000000..baea586228 --- /dev/null +++ b/protected/humhub/modules/notification/widgets/NotificationSettingsForm.php @@ -0,0 +1,38 @@ +render('notificationSettingsForm', [ + 'form' => $this->form, + 'model' => $this->model, + 'user' => $this->user + ]); + } +} diff --git a/protected/humhub/modules/notification/widgets/views/notificationSettingsForm.php b/protected/humhub/modules/notification/widgets/views/notificationSettingsForm.php new file mode 100644 index 0000000000..a9185fe0c4 --- /dev/null +++ b/protected/humhub/modules/notification/widgets/views/notificationSettingsForm.php @@ -0,0 +1,43 @@ + +
    + + + + + + targets($user) as $target): ?> + + + + + + categories($user) as $category): ?> + + + + + targets($user) as $target): ?> + + + + + + +
    + getTitle(); ?> +
    + getTitle() ?> + + getDescription() ?> + + +
    +
    + diff --git a/protected/humhub/modules/post/resources/js/humhub.post.js b/protected/humhub/modules/post/resources/js/humhub.post.js index 197ba1eaef..b01b5bd3ee 100644 --- a/protected/humhub/modules/post/resources/js/humhub.post.js +++ b/protected/humhub/modules/post/resources/js/humhub.post.js @@ -53,6 +53,7 @@ humhub.module('post', function(module, require, $) { }; Post.prototype.toggleCollapse = function() { + debugger; if(this.$.data('state') === 'collapsed') { this.expand(); } else { diff --git a/protected/humhub/modules/post/widgets/views/wallEntry.php b/protected/humhub/modules/post/widgets/views/wallEntry.php index 91293bb845..11ce8233b8 100644 --- a/protected/humhub/modules/post/widgets/views/wallEntry.php +++ b/protected/humhub/modules/post/widgets/views/wallEntry.php @@ -1,3 +1,5 @@ - - $post->message, 'record' => $post]) ?> - +
    +
    + $post->message, 'record' => $post]) ?> +
    +
    diff --git a/protected/humhub/modules/space/behaviors/SpaceModelMembership.php b/protected/humhub/modules/space/behaviors/SpaceModelMembership.php index 76dc3227f9..d7b67e128b 100644 --- a/protected/humhub/modules/space/behaviors/SpaceModelMembership.php +++ b/protected/humhub/modules/space/behaviors/SpaceModelMembership.php @@ -46,7 +46,7 @@ class SpaceModelMembership extends Behavior return false; } - + /** * Checks if a given Userid is allowed to leave this space. * A User is allowed to leave, if the can_cancel_membership flag in the space_membership table is 1. If it is 2, the decision is delegated to the space. @@ -56,17 +56,17 @@ class SpaceModelMembership extends Behavior */ public function canLeave($userId = "") { - + // Take current userid if none is given if ($userId == "") $userId = Yii::$app->user->id; - + $membership = $this->getMembership($userId); if ($membership != null && !empty($membership->can_cancel_membership)) { return $membership->can_cancel_membership === 1 || ($membership->can_cancel_membership === 2 && !empty($this->owner->members_can_leave)); - } - + } + return false; } @@ -182,8 +182,9 @@ class SpaceModelMembership extends Behavior */ public function getMembership($userId = "") { - if ($userId == "") + if ($userId == "") { $userId = Yii::$app->user->id; + } return Membership::findOne(['user_id' => $userId, 'space_id' => $this->owner->id]); } @@ -209,7 +210,7 @@ class SpaceModelMembership extends Behavior $userInvite = Invite::findOne(['email' => $email]); // No invite yet - if ($userInvite == null) { + if ($userInvite == null) { // Invite EXTERNAL user $userInvite = new Invite(); $userInvite->email = $email; @@ -224,12 +225,12 @@ class SpaceModelMembership extends Behavior $userInvite->user_originator_id = $originatorUserId; $userInvite->space_invite_id = $this->owner->id; } - - if($userInvite->validate() && $userInvite->save()) { + + if ($userInvite->validate() && $userInvite->save()) { $userInvite->sendInviteMail(); return true; - } - + } + return false; } @@ -279,51 +280,58 @@ class SpaceModelMembership extends Behavior * If user is applicant approve it. * * @param type $userId - * @param type $originatorUserId + * @param type $originatorId */ - public function inviteMember($userId, $originatorUserId) + public function inviteMember($userId, $originatorId) { $membership = $this->getMembership($userId); if ($membership != null) { - - // User is already member - if ($membership->status == Membership::STATUS_MEMBER) { - return; - } - - // User requested already membership, just approve him - if ($membership->status == Membership::STATUS_APPLICANT) { - $this->addMember(Yii::$app->user->id); - return; - } - - // Already invite, reinvite him - if ($membership->status == Membership::STATUS_INVITED) { - // Remove existing notification - $notification = new \humhub\modules\space\notifications\Invite; - $notification->source = $this->owner; - $notification->delete(User::findOne(['id' => $userId])); + switch ($membership->status) { + case Membership::STATUS_APPLICANT: + // If user is an applicant of this space add user and return. + $this->addMember(Yii::$app->user->id); + case Membership::STATUS_MEMBER: + // If user is already a member just ignore the invitation. + return; + case Membership::STATUS_INVITED: + // If user is already invited, remove old invite notification and retrigger + $oldNotification = new \humhub\modules\space\notifications\Invite(['source' => $this->owner]); + $oldNotification->delete(User::findOne(['id' => $userId])); + break; } } else { - $membership = new Membership; + $membership = new Membership([ + 'space_id' => $this->owner->id, + 'user_id' => $userId, + 'status' => Membership::STATUS_INVITED, + 'group_id' => Space::USERGROUP_MEMBER + ]); } + // Update or set originator + $membership->originator_user_id = $originatorId; - $membership->space_id = $this->owner->id; - $membership->user_id = $userId; - $membership->originator_user_id = $originatorUserId; - - $membership->status = Membership::STATUS_INVITED; - $membership->group_id = Space::USERGROUP_MEMBER; - - if (!$membership->save()) { + if ($membership->save()) { + $this->sendInviteNotification($userId, $originatorId); + } else { throw new \yii\base\Exception("Could not save membership!" . print_r($membership->getErrors(), 1)); } + } + + /** + * Sends an Invite Notification to the given user. + * + * @param type $userId + * @param type $originatorId + */ + protected function sendInviteNotification($userId, $originatorId) + { + $notification = new \humhub\modules\space\notifications\Invite([ + 'source' => $this->owner, + 'originator' => User::findOne(['id' => $originatorId]) + ]); - $notification = new \humhub\modules\space\notifications\Invite; - $notification->source = $this->owner; - $notification->originator = User::findOne(['id' => $originatorUserId]); $notification->send(User::findOne(['id' => $userId])); } @@ -412,8 +420,9 @@ class SpaceModelMembership extends Behavior */ public function removeMember($userId = "") { - if ($userId == "") + if ($userId == "") { $userId = Yii::$app->user->id; + } $user = User::findOne(['id' => $userId]); diff --git a/protected/humhub/modules/space/controllers/MembershipController.php b/protected/humhub/modules/space/controllers/MembershipController.php index e6f19b5fac..b250cf573a 100644 --- a/protected/humhub/modules/space/controllers/MembershipController.php +++ b/protected/humhub/modules/space/controllers/MembershipController.php @@ -157,7 +157,7 @@ class MembershipController extends \humhub\modules\content\components\ContentCon // Check Permissions to Invite if (!$space->canInvite()) { - throw new HttpException(403, 'Access denied - You cannot invite members!'); + throw new HttpException(403, Yii::t('SpaceModule.controllers_MembershipController', 'Access denied - You cannot invite members!')); } $model = new \humhub\modules\space\models\forms\InviteForm(); diff --git a/protected/humhub/modules/space/models/Membership.php b/protected/humhub/modules/space/models/Membership.php index 3cf44adad3..2a407e8afb 100644 --- a/protected/humhub/modules/space/models/Membership.php +++ b/protected/humhub/modules/space/models/Membership.php @@ -9,10 +9,7 @@ namespace humhub\modules\space\models; use Yii; -use humhub\modules\comment\models\Comment; use humhub\modules\user\models\User; -use humhub\modules\content\models\WallEntry; -use humhub\modules\activity\models\Activity; /** diff --git a/protected/humhub/modules/space/notifications/ApprovalRequest.php b/protected/humhub/modules/space/notifications/ApprovalRequest.php index b715d8af6d..2bd3d63493 100644 --- a/protected/humhub/modules/space/notifications/ApprovalRequest.php +++ b/protected/humhub/modules/space/notifications/ApprovalRequest.php @@ -29,11 +29,19 @@ class ApprovalRequest extends BaseNotification * @inheritdoc */ public $markAsSeenOnClick = false; + + /** + * @inheritdoc + */ + public function category() + { + return new SpaceMemberNotificationCategory; + } /** * @inheritdoc */ - public function getAsHtml() + public function html() { return Yii::t('SpaceModule.notification', '{displayName} requests membership for the space {spaceName}', array( '{displayName}' => Html::tag('strong', Html::encode($this->originator->displayName)), diff --git a/protected/humhub/modules/space/notifications/ApprovalRequestAccepted.php b/protected/humhub/modules/space/notifications/ApprovalRequestAccepted.php index 29dddc2f38..d0ffdbcc62 100644 --- a/protected/humhub/modules/space/notifications/ApprovalRequestAccepted.php +++ b/protected/humhub/modules/space/notifications/ApprovalRequestAccepted.php @@ -25,10 +25,18 @@ class ApprovalRequestAccepted extends BaseNotification */ public $moduleId = "space"; + /** + * @inheritdoc + */ + public function category() + { + return new SpaceMemberNotificationCategory; + } + /** * @inheritdoc */ - public function getAsHtml() + public function html() { return Yii::t('SpaceModule.notification', '{displayName} approved your membership for the space {spaceName}', array( '{displayName}' => Html::tag('strong', Html::encode($this->originator->displayName)), diff --git a/protected/humhub/modules/space/notifications/ApprovalRequestDeclined.php b/protected/humhub/modules/space/notifications/ApprovalRequestDeclined.php index 91b33dc2e3..4b98f9dfe8 100644 --- a/protected/humhub/modules/space/notifications/ApprovalRequestDeclined.php +++ b/protected/humhub/modules/space/notifications/ApprovalRequestDeclined.php @@ -24,11 +24,19 @@ class ApprovalRequestDeclined extends BaseNotification * @inheritdoc */ public $moduleId = "space"; + + /** + * @inheritdoc + */ + public function category() + { + return new SpaceMemberNotificationCategory; + } /** * @inheritdoc */ - public function getAsHtml() + public function html() { return Yii::t('SpaceModule.notification', '{displayName} declined your membership request for the space {spaceName}', array( '{displayName}' => Html::tag('strong', Html::encode($this->originator->displayName)), diff --git a/protected/humhub/modules/space/notifications/Invite.php b/protected/humhub/modules/space/notifications/Invite.php index 6b98b4ed8d..f12c365d70 100644 --- a/protected/humhub/modules/space/notifications/Invite.php +++ b/protected/humhub/modules/space/notifications/Invite.php @@ -29,11 +29,19 @@ class Invite extends BaseNotification * @inheritdoc */ public $markAsSeenOnClick = false; + + /** + * @inheritdoc + */ + public function category() + { + return new SpaceMemberNotificationCategory; + } /** * @inheritdoc */ - public function getAsHtml() + public function html() { return Yii::t('SpaceModule.notification', '{displayName} invited you to the space {spaceName}', array( '{displayName}' => Html::tag('strong', Html::encode($this->originator->displayName)), diff --git a/protected/humhub/modules/space/notifications/InviteAccepted.php b/protected/humhub/modules/space/notifications/InviteAccepted.php index 02d34f58fe..3ab68cace7 100644 --- a/protected/humhub/modules/space/notifications/InviteAccepted.php +++ b/protected/humhub/modules/space/notifications/InviteAccepted.php @@ -26,11 +26,19 @@ class InviteAccepted extends BaseNotification * @inheritdoc */ public $moduleId = "space"; + + /** + * @inheritdoc + */ + public function category() + { + return new SpaceMemberNotificationCategory; + } /** * @inheritdoc */ - public function getAsHtml() + public function html() { return Yii::t('SpaceModule.notification', '{displayName} accepted your invite for the space {spaceName}', array( '{displayName}' => Html::tag('strong', Html::encode($this->originator->displayName)), diff --git a/protected/humhub/modules/space/notifications/InviteDeclined.php b/protected/humhub/modules/space/notifications/InviteDeclined.php index 6a187bee62..e193928942 100644 --- a/protected/humhub/modules/space/notifications/InviteDeclined.php +++ b/protected/humhub/modules/space/notifications/InviteDeclined.php @@ -26,11 +26,19 @@ class InviteDeclined extends BaseNotification * @inheritdoc */ public $moduleId = "space"; + + /** + * @inheritdoc + */ + public function category() + { + return new SpaceMemberNotificationCategory; + } /** * @inheritdoc */ - public function getAsHtml() + public function html() { return Yii::t('SpaceModule.notification', '{displayName} declined your invite for the space {spaceName}', array( '{displayName}' => Html::tag('strong', Html::encode($this->originator->displayName)), diff --git a/protected/humhub/modules/space/notifications/SpaceMemberNotificationCategory.php b/protected/humhub/modules/space/notifications/SpaceMemberNotificationCategory.php new file mode 100644 index 0000000000..1edfb32eb7 --- /dev/null +++ b/protected/humhub/modules/space/notifications/SpaceMemberNotificationCategory.php @@ -0,0 +1,52 @@ +id === \humhub\modules\notification\components\MailNotificationTarget::getId()) { + return true; + } else if ($target->id === \humhub\modules\notification\components\WebNotificationTarget::getId()) { + return true; + } else if ($target->id === \humhub\modules\notification\components\MobileNotificationTarget::getId()) { + return true; + } + + return $target->defaultSetting; + } +} diff --git a/protected/humhub/modules/space/widgets/InviteModal.php b/protected/humhub/modules/space/widgets/InviteModal.php index 0bf577591b..b02bb365b3 100644 --- a/protected/humhub/modules/space/widgets/InviteModal.php +++ b/protected/humhub/modules/space/widgets/InviteModal.php @@ -1,11 +1,4 @@ see('This is my stream test post', '.wall-entry'); $I->amGoingTo('Delte my new post'); - $I->click('.preferences', $newEntrySelector); + $I->click('.preferences .dropdown-toggle', $newEntrySelector); $I->wait(1); $I->click('Delete'); @@ -49,7 +49,7 @@ class StreamCest $I->see('This is my stream test post', '.wall-entry'); $I->amGoingTo('Archive my new post'); - $I->click('.preferences', $newEntrySelector); + $I->click('.preferences .dropdown-toggle', $newEntrySelector); $I->waitForText('Move to archive', 10); $I->click('Move to archive', $newEntrySelector); @@ -70,7 +70,7 @@ class StreamCest $I->amGoingTo('unarchive this post again'); - $I->click('.preferences', $newEntrySelector); + $I->click('.preferences .dropdown-toggle', $newEntrySelector); $I->waitForText('Unarchive', 10); $I->click('Unarchive', $newEntrySelector); @@ -80,7 +80,7 @@ class StreamCest $I->dontSee('Archived', $newEntrySelector); $I->amGoingTo('archive the post again with include archived filter'); - $I->click('.preferences', $newEntrySelector); + $I->click('.preferences .dropdown-toggle', $newEntrySelector); $I->waitForText('Move to archive', 10); $I->click('Move to archive', $newEntrySelector); $I->seeSuccess('The content has been archived.'); @@ -112,7 +112,7 @@ class StreamCest $I->see('This is my second stream test post', '.s2_streamContent div:nth-child(1)'); $I->amGoingTo('stick my first entry'); - $I->click('.preferences', $newEntrySelector); + $I->click('.preferences .dropdown-toggle', $newEntrySelector); $I->waitForText('Stick', 10); $I->click('Stick', $newEntrySelector); $I->seeSuccess('The content has been sticked.'); @@ -121,7 +121,7 @@ class StreamCest $I->see('Sticked', $newEntrySelector); $I->amGoingTo('unstick my first entry'); - $I->click('.preferences', $newEntrySelector); + $I->click('.preferences .dropdown-toggle', $newEntrySelector); $I->waitForText('Unstick', 10); $I->click('Unstick', $newEntrySelector); $I->seeSuccess('The content has been unsticked.'); @@ -144,13 +144,13 @@ class StreamCest $I->see('This is my first stream test post', '.wall-entry'); $I->amGoingTo('edit load the edit form'); - $I->click('.preferences', $newEntrySelector); + $I->click('.preferences .dropdown-toggle', $newEntrySelector); $I->waitForText('Edit', 10); $I->click('Edit', $newEntrySelector); $I->waitForElementVisible($newEntrySelector . ' .content_edit', 20); $I->amGoingTo('cancel my edit'); - $I->click('.preferences', $newEntrySelector); + $I->click('.preferences .dropdown-toggle', $newEntrySelector); $I->waitForText('Cancel Edit', 10); $I->click('Cancel Edit', $newEntrySelector); $I->waitForElementNotVisible($newEntrySelector . ' .content_edit', 20); @@ -158,7 +158,7 @@ class StreamCest $I->see('This is my first stream test post!', $newEntrySelector); $I->amGoingTo('edit my new post'); - $I->click('.preferences', $newEntrySelector); + $I->click('.preferences .dropdown-toggle', $newEntrySelector); $I->waitForText('Edit', 10); $I->click('Edit', $newEntrySelector); @@ -189,7 +189,7 @@ class StreamCest $I->amGoingTo('Delete my new post again.'); $I->dontSee('This space is still empty!'); $I->seeElement('#filter'); - $I->click('.preferences', '[data-stream-entry]:nth-of-type(1)'); + $I->click('.preferences .dropdown-toggle', '[data-stream-entry]:nth-of-type(1)'); $I->wait(1); $I->click('Delete'); diff --git a/protected/humhub/modules/user/models/Follow.php b/protected/humhub/modules/user/models/Follow.php index e880fd5eb3..c01bb2d796 100644 --- a/protected/humhub/modules/user/models/Follow.php +++ b/protected/humhub/modules/user/models/Follow.php @@ -59,21 +59,19 @@ class Follow extends \yii\db\ActiveRecord public function afterSave($insert, $changedAttributes) { - if ($insert && $this->object_model == User::className()) { - $notification = new \humhub\modules\user\notifications\Followed(); - $notification->originator = $this->user; - $notification->send($this->getTarget()); - - $activity = new \humhub\modules\user\activities\UserFollow(); - $activity->source = $this; - $activity->container = $this->user; - $activity->originator = $this->user; - $activity->create(); + \humhub\modules\user\notifications\Followed::instance() + ->from($this->user) + ->about($this) + ->send($this->getTarget()); + + \humhub\modules\user\activities\UserFollow::instance() + ->from($this->user) + ->container($this->user) + ->about($this) + ->save(); } - - return parent::afterSave($insert, $changedAttributes); } @@ -86,7 +84,7 @@ class Follow extends \yii\db\ActiveRecord $notification->originator = $this->user; $notification->delete($this->getTarget()); - foreach (Activity::findAll(['object_model' => $this->className(), 'object_id' => $this->id]) as $activity) { + foreach (Activity::findAll(['source_class' => $this->className(), 'source_pk' => $this->id]) as $activity) { $activity->delete(); } } diff --git a/protected/humhub/modules/user/models/forms/EditGroupForm.php b/protected/humhub/modules/user/models/forms/EditGroupForm.php index ce91b17b93..a1309b666d 100644 --- a/protected/humhub/modules/user/models/forms/EditGroupForm.php +++ b/protected/humhub/modules/user/models/forms/EditGroupForm.php @@ -1,11 +1,4 @@ Html::tag('strong', Html::encode($this->originator->displayName)), diff --git a/protected/humhub/modules/user/notifications/Mentioned.php b/protected/humhub/modules/user/notifications/Mentioned.php index 773404c42e..deb9acf5df 100644 --- a/protected/humhub/modules/user/notifications/Mentioned.php +++ b/protected/humhub/modules/user/notifications/Mentioned.php @@ -13,8 +13,8 @@ use yii\bootstrap\Html; use humhub\modules\notification\components\BaseNotification; /** - * MentionedNotification is fired to all users which are mentionied - * in a HActiveRecordContent or HActiveRecordContentAddon + * Mentioned Notification is fired to all users which are mentionied + * in a ContentActiveRecord or ContentAddonActiveRecord */ class Mentioned extends BaseNotification { @@ -29,8 +29,7 @@ class Mentioned extends BaseNotification */ public function send(\humhub\modules\user\models\User $user) { - // Do additional access check here, because the mentioned user may have - // no access to the content + // Do additional access check here, because the mentioned user may have no access to the content if (!$this->source->content->canRead($user->id)) { return; } @@ -41,15 +40,7 @@ class Mentioned extends BaseNotification /** * @inheritdoc */ - public static function getTitle() - { - return Yii::t('UserModule.notification', 'Mentioned'); - } - - /** - * @inheritdoc - */ - public function getAsHtml() + public function html() { return Yii::t('UserModule.notification', '{displayName} mentioned you in {contentTitle}.', array( 'displayName' => Html::tag('strong', Html::encode($this->originator->displayName)), diff --git a/protected/humhub/tests/codeception/_support/AcceptanceTester.php b/protected/humhub/tests/codeception/_support/AcceptanceTester.php index ff79d0eca4..5188bd51c0 100644 --- a/protected/humhub/tests/codeception/_support/AcceptanceTester.php +++ b/protected/humhub/tests/codeception/_support/AcceptanceTester.php @@ -186,7 +186,7 @@ class AcceptanceTester extends \Codeception\Actor public function seeInNotifications($text) { - $this->click('.notifications'); + $this->click('.notifications .fa-bell'); $this->waitForText('Notifications', 5, '.notifications'); $this->waitForText($text, 5, '.notifications'); $this->click('.notifications'); diff --git a/protected/humhub/tests/codeception/fixtures/data/setting.php b/protected/humhub/tests/codeception/fixtures/data/setting.php index cd67357233..55d3feb890 100644 --- a/protected/humhub/tests/codeception/fixtures/data/setting.php +++ b/protected/humhub/tests/codeception/fixtures/data/setting.php @@ -1,24 +1,24 @@ 'name', 'value' => 'HumHub', 'module_id' => NULL), - array('name' => 'baseUrl', 'value' => 'http://dev2/humhub_test', 'module_id' => NULL), - array('name' => 'paginationSize', 'value' => '10', 'module_id' => NULL), - array('name' => 'displayNameFormat', 'value' => '{profile.firstname} {profile.lastname}', 'module_id' => NULL), - array('name' => 'authInternal', 'value' => '1', 'module_id' => 'authentication'), - array('name' => 'auth.needApproval', 'value' => '0', 'module_id' => 'user'), - array('name' => 'auth.anonymousRegistration', 'value' => '1', 'module_id' => 'user'), - array('name' => 'auth.internalUsersCanInvite', 'value' => '1', 'module_id' => 'user'), - array('name' => 'mailer.transportType', 'value' => 'php', 'module_id' => 'base'), - array('name' => 'mailer.systemEmailAddress', 'value' => 'social@example.com', 'module_id' => 'base'), - array('name' => 'mailer.systemEmailName', 'value' => 'My Social Network', 'module_id' => 'base'), - array('name' => 'receive_email_activities', 'value' => '1', 'module_id' => 'activity'), - array('name' => 'receive_email_notifications', 'value' => '2', 'module_id' => 'notification'), - array('name' => 'maxFileSize', 'value' => '1048576', 'module_id' => 'file'), - array('name' => 'forbiddenExtensions', 'value' => 'exe', 'module_id' => 'file'), - array('name' => 'cache.class', 'value' => 'CFileCache', 'module_id' => 'base'), - array('name' => 'cache.expireTime', 'value' => '3600', 'module_id' => 'base'), - array('name' => 'installationId', 'value' => '99846c45e9b9b0962238986a6fed519a', 'module_id' => 'admin'), - array('name' => 'theme', 'value' => 'HumHub', 'module_id' => 'base'), - array('name' => 'tour', 'value' => '1', 'module_id' => 'base'), -); +return [ + ['name' => 'name', 'value' => 'HumHub', 'module_id' => NULL], + ['name' => 'baseUrl', 'value' => 'http://dev2/humhub_test', 'module_id' => NULL], + ['name' => 'paginationSize', 'value' => '10', 'module_id' => NULL], + ['name' => 'displayNameFormat', 'value' => '{profile.firstname} {profile.lastname}', 'module_id' => NULL], + ['name' => 'authInternal', 'value' => '1', 'module_id' => 'authentication'], + ['name' => 'auth.needApproval', 'value' => '0', 'module_id' => 'user'], + ['name' => 'auth.anonymousRegistration', 'value' => '1', 'module_id' => 'user'], + ['name' => 'auth.internalUsersCanInvite', 'value' => '1', 'module_id' => 'user'], + ['name' => 'mailer.transportType', 'value' => 'php', 'module_id' => 'base'], + ['name' => 'mailer.systemEmailAddress', 'value' => 'social@example.com', 'module_id' => 'base'], + ['name' => 'mailer.systemEmailName', 'value' => 'My Social Network', 'module_id' => 'base'], + ['name' => 'receive_email_activities', 'value' => '1', 'module_id' => 'activity'], + ['name' => 'receive_email_notifications', 'value' => '2', 'module_id' => 'notification'], + ['name' => 'maxFileSize', 'value' => '1048576', 'module_id' => 'file'], + ['name' => 'forbiddenExtensions', 'value' => 'exe', 'module_id' => 'file'], + ['name' => 'cache.class', 'value' => 'CFileCache', 'module_id' => 'base'], + ['name' => 'cache.expireTime', 'value' => '3600', 'module_id' => 'base'], + ['name' => 'installationId', 'value' => '99846c45e9b9b0962238986a6fed519a', 'module_id' => 'admin'], + ['name' => 'theme', 'value' => 'HumHub', 'module_id' => 'base'], + ['name' => 'tour', 'value' => '1', 'module_id' => 'base'], +]; diff --git a/protected/humhub/tests/config/test.php b/protected/humhub/tests/config/test.php new file mode 100644 index 0000000000..f036450557 --- /dev/null +++ b/protected/humhub/tests/config/test.php @@ -0,0 +1,7 @@ + [ + 'default' + ] +]; \ No newline at end of file diff --git a/protected/humhub/widgets/JsWidget.php b/protected/humhub/widgets/JsWidget.php index 71bfb1f8b0..7e6eceb1f0 100644 --- a/protected/humhub/widgets/JsWidget.php +++ b/protected/humhub/widgets/JsWidget.php @@ -1,11 +1,4 @@ $hit[0], 'class' => 'atwho-emoji', 'width' => '18', 'height' => '18', 'alt' => $hit[1])); + return Html::img(Yii::getAlias("@web/img/emoji/" . $hit[1] . ".svg"), array('data-emoji-name' => $hit[0], 'data-richtext-feature' => '', 'data-guid' => "@-emoji".$hit[0], 'class' => 'atwho-emoji', 'width' => '18', 'height' => '18', 'alt' => $hit[1])); } return ''; } @@ -166,7 +166,7 @@ REGEXP; $user = \humhub\modules\user\models\User::findOne(['guid' => $hit[2]]); if ($user !== null) { if ($buildAnchors) { - return '
    @' . Html::encode($user->getDisplayName()) . '​' . $hit[3]; + return ' @' . Html::encode($user->getDisplayName()) . '​' . $hit[3]; } return " @" . Html::encode($user->getDisplayName()) . $hit[3]; } @@ -175,7 +175,7 @@ REGEXP; if ($space !== null) { if ($buildAnchors) { - return ' @' . Html::encode($space->name) . '​' . $hit[3]; + return ' @' . Html::encode($space->name) . '​' . $hit[3]; } return " @" . Html::encode($space->name) . $hit[3]; } diff --git a/resources/js/pagedown/.hg/00changelog.i b/resources/js/pagedown/.hg/00changelog.i new file mode 100644 index 0000000000000000000000000000000000000000..d3a8311050e54c57c5be7cfe169e60a95768812c GIT binary patch literal 57 zcmWN_K?=Yi3U;-P=;W=_0!Y{p9t?m6p>CQfE#zyk6>V*jB4$mA(VOwLYBPu0sR&d)=a6!J|h z%1+5I&(m{F$t=k)0xCjRB3YDLoL^d$oLa13o|~eZR9aG!pI59`P@ICUQWS2ObADb~ zYEellHuI!%@{<#D^x5dvK`bne697r-7682m_5`Nu#lS`+ z73G%~rxrm>$tcOq!R_?m#JtRs%qpx-7sWIzxfmnBxP3gGUHyVx^-3yA(Cy(0a&>g^ M#VW;*61sWm0J%HR=l}o! literal 0 HcmV?d00001 diff --git a/resources/js/pagedown/.hg/hgrc b/resources/js/pagedown/.hg/hgrc new file mode 100644 index 0000000000..50fe560cee --- /dev/null +++ b/resources/js/pagedown/.hg/hgrc @@ -0,0 +1,2 @@ +[paths] +default = https://code.google.com/p/pagedown/ diff --git a/resources/js/pagedown/.hg/requires b/resources/js/pagedown/.hg/requires new file mode 100644 index 0000000000..c1674f2dd0 --- /dev/null +++ b/resources/js/pagedown/.hg/requires @@ -0,0 +1,4 @@ +revlogv1 +fncache +store +dotencode diff --git a/resources/js/pagedown/.hg/store/00changelog.i b/resources/js/pagedown/.hg/store/00changelog.i new file mode 100644 index 0000000000000000000000000000000000000000..6583bdeacd17ba4689737055539a72984462f919 GIT binary patch literal 14624 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 LkCP`E`n>-SW+?f? literal 0 HcmV?d00001 diff --git a/resources/js/pagedown/.hg/store/00manifest.i b/resources/js/pagedown/.hg/store/00manifest.i new file mode 100644 index 0000000000000000000000000000000000000000..b0ef64e00c1ae849d57bd87421d6a7b009901ab5 GIT binary patch literal 12846 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 az@~mLI*t#p4)*eE^1gG}a(h`z_WuFYvoM(e literal 0 HcmV?d00001 diff --git a/resources/js/pagedown/.hg/store/data/_l_i_c_e_n_s_e.txt.i b/resources/js/pagedown/.hg/store/data/_l_i_c_e_n_s_e.txt.i new file mode 100644 index 0000000000000000000000000000000000000000..b97372b1020cbae6affd38dd4183bf7e68e55c95 GIT binary patch literal 1181 zcmZQzWME_f17=SK2G%nWHUq zs(f>Q|IEgNi)*IsE3T4H$d6^%-MF;Gf3NK+t%E;^Kdl# z{0N0bf!nSNMqBN@>ejb@Swhx#bsauNyVC0)UOzv-|8GKEi9D{tx`R1a#8KwvD30Lr4nxxkE9mvQl7-K<>fJF zf4#VM?t3qbckYO~QaoSHqdR(r>uSk2D+<4?PTKT$q3l+9qZg)y+uIvl{g3)dIwel( zEPZgb*8kjFK&u7R`&anyR{UyJu|)4O#eI z+WOJ|mP@7AzF77Bl;&M`(EH}C=i94Oa~yW3i<`9lJ^wIn-Jgcr+-rB0@J=<=6TBLy zWLflpS+C(3@3Q|zch>L+K7MDy%~xuf|1R>V>j7bzO4C-y6zFs%&03$nXh(B2i&!f2&WJ9y$k{?3 z9}lTk_~v-W`HEMhY*!LL>pV@Z_@c-WD}C*R4N0NO%U;h3-4GSNdegIvgtk4c8qY*e zeNfe(B|YV+)Q9&K4OIs}&DnCPWv!uut5{;xIytSv?HLJy&y4$yM)%#jk$diZ)Qs#| z#v9Kh9Gmv0&po8mM56!RT;4-}n}T~qB2@zWl-%aH`k838e%3t3)h@oa zGsB+>83*|b2Dyo`i2d<#{@Yfbd{Dh4?)smpf@yP}dbAm9-Qzd(`)WUT%8gP`W&&kF zAn-!Uh`JyFP+qJ|c(~oMoO$Zw-;I-O*R$P8t5nPe$sqt^6$1m~4j|voH$NpaEi*Z> zBr`v+SRpYlMIos)JuR~$wOBzTSyREtz`#=1$iUE8A-E(lIa|TCA~_>5FFjSkGcQ?> zmkVSf^JK8O$SyGiiZFs)^7>1}`BO_$m;Tsg^Rx9i+nkjvU4DaA!wHgIVuIZz09Yve AlK=n! literal 0 HcmV?d00001 diff --git a/resources/js/pagedown/.hg/store/data/_markdown._converter.js.i b/resources/js/pagedown/.hg/store/data/_markdown._converter.js.i new file mode 100644 index 0000000000000000000000000000000000000000..f30272390e92710c2e76e9e64879a76294907896 GIT binary patch literal 27633 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 F{ucxQ;Mf2F literal 0 HcmV?d00001 diff --git a/resources/js/pagedown/.hg/store/data/_markdown._sanitizer.js.i b/resources/js/pagedown/.hg/store/data/_markdown._sanitizer.js.i new file mode 100644 index 0000000000000000000000000000000000000000..f14166a8d9fb3566f2f7c09b66ce86746f51cbc3 GIT binary patch literal 2201 zcmXw)cRUpSAII`WSvgQ$VJwfb?&k<@|BsL ztjr{2l|8@cE9;A&-|L^}>+yNN9-qhK{r=|#00Mx36EUy=0BjQf^8mo%sbp;~k`6n- zJl>kwS>XKQ$*fzQEC0_N(?6YTW$p7_k^ON`ap*UM7(Dv&r`6>V=xInuGI6h1) zu@dNH5w`0Il?y7MI?*-wPYpe}OL0$|W-)iO`8Q^mAr+vYE(f4r^}uA-SWq&U@Vu&;plJ$+P-4_rdd}mk;DX(F{Bh z9)%-Yu7OZ-)u|x(K1%D}FJ;K9&l?5;+akh+-u`bC+Yhg)3);CE=n;%7+bw*Hb}e4>NkW>d+xw zmEl2Q!=0MC8#0}md*({+8`yZA`d_`cOS6js$r8A360*w-_@UD&Y3dO9O5dxZ53|m< zA+;{P^NAdd`Y(gTM=oV|xo65gRcvV&G*1k>lunj`+m)wgO({0W%a_99orWq2Zd#t zxOc#UGW2ru))rKi9iQ`FFI#JynEMwfppmM8B}Qo{KE7~d2*sHA8Ou4JBYrWbP`sDi(d0jQ z+?M2^{q3gRIIG#!qHZr_ye70){93t<2=qk)Q`Y3ZP?D0&Ei&w0Y+}ryfPtu#kQ!8Q zI$=7!gf*)%V!>qDMb|9ScR1 zdG-6a-VoelZ|{xWpJn}|{%PeOpfZErDiMuCK^=`hf`Nq2;Wb_9u zx7}%jb$K{3Sm6NAIiyU(!FniZ^AW;AKW#O<%tvt?qNkGhN~Rvco;T!q_i zl(g2ppb?J^_|nX{lzc<=B4Lx!gMU6Uk>Ze~ z&~Hkhf;0Ti7>0Y*s}3tsZX%gHWeH)$YEXF{za~s`i@D6fzRrI($lQ5s|fZqqo86bbPhBE;M&)LS(2sGZ z0i6n~#_W8WN2&sM?c+u$$fa4@7o`F2VL`C6omP~}92?{=q|Q?Lo2mJmZQL=tlS z?C~?h<&)va(M0YvcH@Y^;~%S3-{q_h*yT|Lx1ShCPfG4WhEq;6CpfC79(7kb+4HUI zj6*Sb$=dMtxeF*sa|zNmU$+^va%w0j1qm(h*SPX!8b3LI?zuF6{(J4=!fzlV7Qog6 zSi2axE92yt#JEU}G8g4RD$IFUyPuyPzfZI$&z5D*eiIi)-FP%C) x1{dvGK+c`LfreuiIh;sp>J(e~JyRHCW|TGwfyK5VKq zImwZDIC1JYqU+TEzB|VdG@(62=iK+*=ey5NCcCyuoiY_nrtoJt_#*8-Gly!fO)51k zI5$X)&aPmzb=Filh{nelZk^S$<>*~t;EOD4jYRAS;PAx}O0aTLtCWztvMyw!8W30p zfwdRt7`+5`#8=GDWVEXGgspzpXn74n29bL&!nq8a)T)|CVwuXJ16@{l6YM;XdRq35m6G0h@d(yoU5;*D0DebO&P({$aqaM%PFVvX~g8N-{;mi9%e~q z?)?jhI5T@)%ACP9 zM-Zw!p5>?RC^&t|Mh{%}v8%agq~GQLCi?0oqWav*a{jB%FWH*dn4brvJ0T<#7I%wr zP&*tB^W2y`2sS8fTHHJ@b{28w;;0*5R9fZ-rwy)lTHn5hh*62>G_Q-k+7tUcxED!R!+oC|3(Z+nayA_Dy zLmY|XW2j#F>OkwyH?V_<<&3fF)l~F3!KUSFQrIA|=~!)*8R9)sfcG8p!;8~o3~aCm uzMp_nrvG5uLhYO{^^NJ8@Z;YD?lQ$j!iw^v;o$9h_%@wPum1v5T4*=#yeljK literal 0 HcmV?d00001 diff --git a/resources/js/pagedown/.hg/store/data/_markdown.local.fr.js.i b/resources/js/pagedown/.hg/store/data/_markdown.local.fr.js.i new file mode 100644 index 0000000000000000000000000000000000000000..0602a1cfb1c1d21f8d30ad86a5ae4b78a91776b9 GIT binary patch literal 650 zcmV;50(JcW0RRC20000000K$?00f}`00000000U9|NsC0|NsBr+N@+Z8Rl;$lx8e+ z5^AJty;AA`00000000000001ZoQ0IpZqqOnhOgVFIO`n`ADEDOC> zQ>k+e5Y<^j<;o`8tBq60~xmZYBlXwXPOGUE|*5injau+5EV{7__l@CF5f99H|a zhXsO5U6PkIDa+EiYIj?M2AqO>GfT3RC8RfLWo>hmB(c|NuImJD23A;upu>k0%A0|A zcTKOHwtl8lnkDqXGD>$D%CY;)&`hWtGT8e&O0ElEe6z8J3+rd`6=03Ql=;VqdK*x_ za3FoxzBzonG;U4JOK+L`J1|l2xAv^t7hnEQqQUdP{nt1Vg5lp;21}a|h0aes{nnIE zrkj~GTpTKci`g)e*6a1O)VhqY7P-l*lhtft5GMgWsdQ_FDu-aXDbdb`Uo*+oa+rS7 zu7$ipH1h@B5R|KFW^H#{id4&(==+e7 zd*s~RL8ArXncLew#)E0G_WLb&+wS`mwO`VGuR>nX5M;Ox(Xi@8ny~6+WSj>^G#$=C z@J4{~@8LXD+K8W8(GQI^ZZZw kLnRhA078SZ@pM7Mwa&o)-)GS8-5&RE$D{G_Z;pb0gK?}qbpQYW literal 0 HcmV?d00001 diff --git a/resources/js/pagedown/.hg/store/data/_r_e_a_d_m_e.txt.i b/resources/js/pagedown/.hg/store/data/_r_e_a_d_m_e.txt.i new file mode 100644 index 0000000000000000000000000000000000000000..2431023a17b1e7b5c96e5db8e264c251f39beab0 GIT binary patch literal 64 ncmZQzWME{#0{%n64&J91ElOJ*kA4VBT`S_X^H2CtJK+(U*JE&wj;^_c!`xKUkBHJ6CzpDt#k@lzsM-J&g2rE7@%@+ zzCwiDY8#Omf>JR}k*%qmI!l&ZFpSE-ucE86@Xpp7f<=3~FParGrfp|cD%>^IH+!um zcd*Vx-FMAl6TPL&|L}ZqNNKuNld$L-?|#|LbN5Zu_e8zsWDeGxe&=1$OyTCWM>sY8 z6y5J{xMS~G^G-wIB;$!fuQRS<6IWzdFtN zM)uCkkJdaob^C+2o4&icC{lOoQ!NXF+>?A9HI8ntwEO#90v=uxS*2RLJ7%Bx{ph|Z zuCRUHZIY8Xnzq{B{x9Qt$3rqQK>KX{3UkNZ@27=c*zh6jYuziBHPwu(`)>yxSthj8B(Rd+t3us=oc*t&)$m+4jO;&+tAgFc7yW zRt`FtUz~gK)5MU@_I1I+-jhEhTrpjDeZs@`hcbKP_deKU#BYA0?&W5M`Ojy~`SOQ# z@1Y~x=l^r=`TbOS_SyJ%*Xv8NJ^Z{UUAx(r@#S&lq{mBK+~4n*a{11-U*aEAL?(d} z7AQFaK@u=gz5}xu7}P*CC|#PL>d#2s&UQS6pPBW2RBmD4(yp^mHDIb@PI5v*L0ZFu z6HDg&dGhAar9YEGTC?=dE%~!($`z%%bFOR=(q>)U#ML_G&!kCLL|9X~xw*6T&NUsG vGUd#kk`;S09a#f(Gz~4S_iF4n(Ld_tXn9oOtb*>@S*xdYb2Cikk@W@uz5;Hn literal 0 HcmV?d00001 diff --git a/resources/js/pagedown/.hg/store/data/demo/browser/demo.html.i b/resources/js/pagedown/.hg/store/data/demo/browser/demo.html.i new file mode 100644 index 0000000000000000000000000000000000000000..8107e7a4c0db9c5a4bf82d04ff414a23c116ee41 GIT binary patch literal 1594 zcmXw2dpOez82&Aek$bMObaPvjH9~fpTj$VfuBC&qR&uwDwsufah+-S2DU>d&%w=Sh z3Ayz&EFmgOb6GU3T7(o%>(u)^-}}AK_q~6;&+`Hx00jJmoHYO_G5^;9u=)!_%GNis zP`3Qn4BhVN(FIhV_eJy=4GsW;rc-N-*MH5q&dU1&28Q$ok( zMIDWmbdv&JwXZhC7um+}{sCXwVluxxcS;xpPa>{}XQl*`%I3odD7V62AyX=GnSXAy zWApoz%n0AF=#eY+);=TLXZgEif(A26KPoC`S4p3mW zh%?@atRe;@u3HqY_pT=j43U#KM#`?&87BKs3vPIPJPNkEKa%p~#y-5Ip?ZG_;!I|j z6-LSI%!(&NuRfOBBZNslsT*n)3)vl=9S}ILVncO@rDOR+7oNjcl$*#I=YzOAFq&$( z@uG1c7a4@7u@O2f7F5c-R0VN6%&c;HV4*oy3LET59^KSCns->ckzH{f6?bD3g#l}q zf<}VUjT&+ro)EOU-Kw`wech>f@(G`6KuD^$i{f1L$!jj{#rYGagH`*PetSjH)Y|kn z6Qg!cER|<}?0B^`1Q*23#!f~Z&1jO`a{K=M>V|-BPAArno1e#Dn?5|epDZpB>DuR4 z@3=={4mRVL}a^sl`f8i)B(b>^m7EcNcdd5#0JVpkU;n zVJapu=DH{sO4}6^ljf&Xv<(fH3((YThwvyz07p`EyK`n%DqrKXqRFR$z672{?W98_ zsJKGUv;%{?6BPH;byDGGJ;$f- zmpA0t4iPXhanZ1fLlcTb{h2nlYEJXS8&3X>wMH%BhuF8BUQ*E8-bI?K&kMgSDQeHD zY411^g;L`x?wP2FI2t)Hxh$p&@osOP&gH~}G02|<#ugDJs`|Lobz^3TWHlQqeq-zk z{C(uDHEybX(cQ7MU{8hb37Q}8rr86!&B%-XJ*7q~P=q7V(F=j`zMVZ2xpg%c66m4w z)|RfIa3Wcyf!>@rA;=q_o^wD$#f~PAd?T(t(Qi?Z+)I3qkt0=(2ERO5b%$`A=#n#n zvujQ?5G4W?lO zHhx0!b0=8%U
    IwW|A)uPTZCj zm-Hr%+dlW-w4bT}(e$s7D*!7?C9#0xl}su*|F+h4ZNGEwl6eW7h(6%qRU*KRJd)X;7}T!mj&Z zldNb9y*84}3zxDBd=M1f@NsBXGXGAdU1myL(drD+YSqd0-1XbD1&_U{dVE3W0)C!8 zLJ$XOYmfb#l4M}MQub&udyjvpkQ9}pBa2KvSjHd-0H}y$>IY1J3`iEWdxlL#{9$gu zlr*!GNcHNORl$4zbo?T65%E{l7+TUe_)^E_h^>siKZzzW37!;~6|{Z+q5DYq z;?5j{sa^+8yuU#W{&>JPbV|4-@*9cG`cRay>7zMuNJCz&IY^##D79*L_3W`;;^oJy zdAfxtrhYf_NW9+mI#7{56HK(t)mo<>wp=#~JxiHARhL*bttHjl<9n9yAF2K2EC2ui literal 0 HcmV?d00001 diff --git a/resources/js/pagedown/.hg/store/data/demo/node/demo.js.i b/resources/js/pagedown/.hg/store/data/demo/node/demo.js.i new file mode 100644 index 0000000000000000000000000000000000000000..54a81fc2a4d7ef68f7083ed967864b91fa2eca30 GIT binary patch literal 787 zcmV+u1MK_&0RRC20000000Pqh00sd70000000030|NsC0|Ns9UZIec&(mqZiOf8(< zJq1G=LMH(L00000000000001ZoQ+i9Z<|02e&?@nuhIZ51v+)olmai+r0!wsq*jxr zbq^PC5*E12?nkn$?f*U>0ZQ9w8$wZRfBx>X&nAxH)78!UDcme84fOu7+5m?V8wn>T zPzf^DN|v@Tl@x$tQzQ z0<17`Mo^1~NYH7f7URV}ewZQ%&ppp|Dk>CmgV(4i5X`OE-)%tZ zCL~dZaObs@uGLCfgTzUFjMiLfbg+mlVsn^kPKgdfQL_}hK{(1XU@>zv&^^Q&ZB=FP z5_uV%C1+iBAEVI;*k`^#5YoV0T2Znm5M!0t+YjyUps&3>(l@%rUT3SS^}EVn%6O9` z6SxCL9Wpe~CuX-sHU(zzmx-6AsTn+UB8J3(r7n`EGg29-vc{7 zhD)q#d2}moMM@}&V*bHzV9VOe zd<@;g-EDJP2ohH7>}^65zt!1k0&bQwzbRiPxu+K%DO}gQz#vZJ#RS-^+MLtqKR+ez zkDqz_xF8%zu%wV4?qea1^6c55C9-U`CkS= zcfX5!L}$O8*L>5o)m%XT^2+2DbB+7of4+?b7RhhI=ee0Ooc0mX^r#))q!6m=?4D_r z1;{5HQZKgB@%D)?F0;LMiKQaDGaM$-{L&I?7Yojeft_AXE}*R*pWbvu+a_I zeV}~dKWgZgzxeMqv=(M8=IRkn5|&|SS67yat98<8aSM{ngzKG_Yet%%Pct5>0b*j> z?P(!?$1)WP1b7d)Q7o*cEJJvEplp9xGdEaj_RXeYcu+%(?)g#fpk{qLPPZ`(Rbc;d zGq^c6C@;{`2*q_<31TvTN$i}`B1)3uw>Z1h=zUKx+H`xhqRo~n_<>8wMy-V!=lR^1 z9b8R$>vlt=ws(KNEHDE<;HGiF3q6YC`HakU$tpU&*7Iq0?oV*adH~Nq7=_DN7>qeU zvCNWQ!jCpldzJ3u_#X2BU#$@oFzfpIThZS@*9U?^bw>C>Qe9P$0KUmq!Mn&mG=$jm zhRvqCG!kzOcW4dw{ZJY+(hk%Wy8^c(pL3Y(dl%}xy>kNdMt;a0pCdzmTXvYiU&M=h z>96kcEX;eW)te2U)~YD!IMr)I1mo`ATwenYroy9G!#;E=2wjFISDL3Oa&R-E`C3zh z9W6KGcM6hw*MKcqX~_hmlANOnsg!^G%exn1*(W8Z=?K&l&AMaEkYQms}<V02Hc(#|m3bto^Fu2xO%QH}G;|K;9YB*D#c`y!la_{zk?U67X zJ87m2^#?1S4Iep|6=aYRImpfcb^Kg+(pW~SA&d2=5AOFC^pkLqF<50eTuMuvs6+Vp+H7 z#naqh!yv2q$`(EqPaW5zE3e#F9wC=r(05rb_HN!@svH#oK?Q`0%Z{Av23E{Ow>nDm*^9i`lSIipYw4EO5M> zoj}7Z_22Araw*KAL{_*Hv;I&er?18OB3L!%Ly=U7=|*>tR_+Ze|Jx`X)XyJY^lXLL zkL@_Wzc_of(K+dkS7`jR2`_}UuuG{2W+xkC86f8vNc^emw2mq-reH+=gN4kas4xV~&KPzsd1a1ttiGqp zaZ${j|8a}sPGi>A9ZulhL!N>gsJneB0tR*n8_9y7lrEP>H{iyI#!cvX)i(rs*f%8hY6>6ze z>0p%UgV(=>#kbL4#<~ZLf5M(|t(QE#j4`QR36jRhVvm>YB}z19>N?hW+UXUa@5^Bm12bOwme+fy@F$!BsonW&Yis`%TT}dIqY8q7=7JH?Ge!Rri9>;ca}XAQA|rS8S>xm zb&~z|{oN8+5m@Y8Bo;dWd=r?GLTcsdl3sYc+j-bJSw)}Jl?f(2oQ#)w#j9Fo?5x`# ypJoW?JGxLkxGgBL28pxZtf51Q6Mv^XR|QQVrt( literal 0 HcmV?d00001 diff --git a/resources/js/pagedown/.hg/store/data/node-pagedown.js.i b/resources/js/pagedown/.hg/store/data/node-pagedown.js.i new file mode 100644 index 0000000000000000000000000000000000000000..e4a1a7cea68ee67d10c7d818dd7b065271fcc830 GIT binary patch literal 145 zcmZQzWME`~fIuKM1Im{D4+T~~F00==W2$sZZb|1w-FTTFw+}W#)qtsrIo@kEPUvfC zUMoI#UfWw&%TqUKL#T$AuFhE-VoM~FcGP^Z)v8g!2xef~p0DBxciU0rr literal 0 HcmV?d00001 diff --git a/resources/js/pagedown/.hg/store/data/package.json.i b/resources/js/pagedown/.hg/store/data/package.json.i new file mode 100644 index 0000000000000000000000000000000000000000..802c6011ef1837ef5cdeaff70cdded95fc7158a6 GIT binary patch literal 413 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 GB{73${*2%N literal 0 HcmV?d00001 diff --git a/resources/js/pagedown/.hg/store/data/wmd-buttons.png.i b/resources/js/pagedown/.hg/store/data/wmd-buttons.png.i new file mode 100644 index 0000000000000000000000000000000000000000..c7d81d4a1d0de294820f5dca2ddae36a8954f7af GIT binary patch literal 7530 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 literal 0 HcmV?d00001 diff --git a/resources/js/pagedown/.hg/store/fncache b/resources/js/pagedown/.hg/store/fncache new file mode 100644 index 0000000000..8acdb65c88 --- /dev/null +++ b/resources/js/pagedown/.hg/store/fncache @@ -0,0 +1,15 @@ +data/package.json.i +data/wmd-buttons.png.i +data/README.txt.i +data/Markdown.Converter.js.i +data/demo/node/demo.js.i +data/Markdown.local.en.js.i +data/resources/wmd-buttons.psd.i +data/demo/browser/demo.html.i +data/LICENSE.txt.i +data/Markdown.Sanitizer.js.i +data/Markdown.local.fr.js.i +data/demo/browser/demo.css.i +data/node-pagedown.js.i +data/local/Markdown.local.fr.js.i +data/Markdown.Editor.js.i diff --git a/resources/js/pagedown/.hg/store/phaseroots b/resources/js/pagedown/.hg/store/phaseroots new file mode 100644 index 0000000000..e69de29bb2 diff --git a/resources/js/pagedown/.hg/store/undo b/resources/js/pagedown/.hg/store/undo new file mode 100644 index 0000000000000000000000000000000000000000..7042087336609707675f2ff51c2b823dced28871 GIT binary patch literal 456 zcmZ{hO%j4I424~L7fY|;APZ;2g`A+Y;YT20+8~an7s>+a;4QdbH~XsHzS zK^~>-6xIbe_M=g(qEy+&(1;?NB(6VT#nbJ`iK&@F&4Q!BYH-omnNvP66FSj5v$Gx( zzQFQ7dP`chq6qo7YnS0#AUyH*z%U=YMWM^SERbYP=-5EXar>eTBq pd=a5!IN=C?76PDlo8r8QwcXu-L2bDF$Uo(goG In Chinese, the smurfs are called 藍精靈, meaning "blue spirits". + * + * turns into + * + * > In Chinese, the smurfs are called QNIhQQMOIQQOuUQ, meaning "blue spirits". + * + * Since everything that is a letter in Unicode is now a letter (or + * several letters) in ASCII, \w and \b should always do the right thing. + * + * After the bold/italic conversion, we decode again; since "Q" was encoded + * alongside all non-ascii characters (as "QBfQ"), and the conversion + * will not generate "Q", the only instances of that letter should be our + * encoded characters. And since the conversion will not break words, the + * "Q...Q" should all still be in one piece. + * + * We're using "Q" as the delimiter because it's probably one of the + * rarest characters, and also because I can't think of any special behavior + * that would ever be triggered by this letter (to use a silly example, if we + * delimited with "H" on the left and "P" on the right, then "Ψ" would be + * encoded as "HTTP", which may cause special behavior). The latter would not + * actually be a huge issue for bold/italic, but may be if we later use it + * in other places as well. + * */ + (function () { + var lettersThatJavaScriptDoesNotKnowAndQ = /[Q\u00aa\u00b5\u00ba\u00c0-\u00d6\u00d8-\u00f6\u00f8-\u02c1\u02c6-\u02d1\u02e0-\u02e4\u02ec\u02ee\u0370-\u0374\u0376-\u0377\u037a-\u037d\u0386\u0388-\u038a\u038c\u038e-\u03a1\u03a3-\u03f5\u03f7-\u0481\u048a-\u0523\u0531-\u0556\u0559\u0561-\u0587\u05d0-\u05ea\u05f0-\u05f2\u0621-\u064a\u0660-\u0669\u066e-\u066f\u0671-\u06d3\u06d5\u06e5-\u06e6\u06ee-\u06fc\u06ff\u0710\u0712-\u072f\u074d-\u07a5\u07b1\u07c0-\u07ea\u07f4-\u07f5\u07fa\u0904-\u0939\u093d\u0950\u0958-\u0961\u0966-\u096f\u0971-\u0972\u097b-\u097f\u0985-\u098c\u098f-\u0990\u0993-\u09a8\u09aa-\u09b0\u09b2\u09b6-\u09b9\u09bd\u09ce\u09dc-\u09dd\u09df-\u09e1\u09e6-\u09f1\u0a05-\u0a0a\u0a0f-\u0a10\u0a13-\u0a28\u0a2a-\u0a30\u0a32-\u0a33\u0a35-\u0a36\u0a38-\u0a39\u0a59-\u0a5c\u0a5e\u0a66-\u0a6f\u0a72-\u0a74\u0a85-\u0a8d\u0a8f-\u0a91\u0a93-\u0aa8\u0aaa-\u0ab0\u0ab2-\u0ab3\u0ab5-\u0ab9\u0abd\u0ad0\u0ae0-\u0ae1\u0ae6-\u0aef\u0b05-\u0b0c\u0b0f-\u0b10\u0b13-\u0b28\u0b2a-\u0b30\u0b32-\u0b33\u0b35-\u0b39\u0b3d\u0b5c-\u0b5d\u0b5f-\u0b61\u0b66-\u0b6f\u0b71\u0b83\u0b85-\u0b8a\u0b8e-\u0b90\u0b92-\u0b95\u0b99-\u0b9a\u0b9c\u0b9e-\u0b9f\u0ba3-\u0ba4\u0ba8-\u0baa\u0bae-\u0bb9\u0bd0\u0be6-\u0bef\u0c05-\u0c0c\u0c0e-\u0c10\u0c12-\u0c28\u0c2a-\u0c33\u0c35-\u0c39\u0c3d\u0c58-\u0c59\u0c60-\u0c61\u0c66-\u0c6f\u0c85-\u0c8c\u0c8e-\u0c90\u0c92-\u0ca8\u0caa-\u0cb3\u0cb5-\u0cb9\u0cbd\u0cde\u0ce0-\u0ce1\u0ce6-\u0cef\u0d05-\u0d0c\u0d0e-\u0d10\u0d12-\u0d28\u0d2a-\u0d39\u0d3d\u0d60-\u0d61\u0d66-\u0d6f\u0d7a-\u0d7f\u0d85-\u0d96\u0d9a-\u0db1\u0db3-\u0dbb\u0dbd\u0dc0-\u0dc6\u0e01-\u0e30\u0e32-\u0e33\u0e40-\u0e46\u0e50-\u0e59\u0e81-\u0e82\u0e84\u0e87-\u0e88\u0e8a\u0e8d\u0e94-\u0e97\u0e99-\u0e9f\u0ea1-\u0ea3\u0ea5\u0ea7\u0eaa-\u0eab\u0ead-\u0eb0\u0eb2-\u0eb3\u0ebd\u0ec0-\u0ec4\u0ec6\u0ed0-\u0ed9\u0edc-\u0edd\u0f00\u0f20-\u0f29\u0f40-\u0f47\u0f49-\u0f6c\u0f88-\u0f8b\u1000-\u102a\u103f-\u1049\u1050-\u1055\u105a-\u105d\u1061\u1065-\u1066\u106e-\u1070\u1075-\u1081\u108e\u1090-\u1099\u10a0-\u10c5\u10d0-\u10fa\u10fc\u1100-\u1159\u115f-\u11a2\u11a8-\u11f9\u1200-\u1248\u124a-\u124d\u1250-\u1256\u1258\u125a-\u125d\u1260-\u1288\u128a-\u128d\u1290-\u12b0\u12b2-\u12b5\u12b8-\u12be\u12c0\u12c2-\u12c5\u12c8-\u12d6\u12d8-\u1310\u1312-\u1315\u1318-\u135a\u1380-\u138f\u13a0-\u13f4\u1401-\u166c\u166f-\u1676\u1681-\u169a\u16a0-\u16ea\u1700-\u170c\u170e-\u1711\u1720-\u1731\u1740-\u1751\u1760-\u176c\u176e-\u1770\u1780-\u17b3\u17d7\u17dc\u17e0-\u17e9\u1810-\u1819\u1820-\u1877\u1880-\u18a8\u18aa\u1900-\u191c\u1946-\u196d\u1970-\u1974\u1980-\u19a9\u19c1-\u19c7\u19d0-\u19d9\u1a00-\u1a16\u1b05-\u1b33\u1b45-\u1b4b\u1b50-\u1b59\u1b83-\u1ba0\u1bae-\u1bb9\u1c00-\u1c23\u1c40-\u1c49\u1c4d-\u1c7d\u1d00-\u1dbf\u1e00-\u1f15\u1f18-\u1f1d\u1f20-\u1f45\u1f48-\u1f4d\u1f50-\u1f57\u1f59\u1f5b\u1f5d\u1f5f-\u1f7d\u1f80-\u1fb4\u1fb6-\u1fbc\u1fbe\u1fc2-\u1fc4\u1fc6-\u1fcc\u1fd0-\u1fd3\u1fd6-\u1fdb\u1fe0-\u1fec\u1ff2-\u1ff4\u1ff6-\u1ffc\u203f-\u2040\u2054\u2071\u207f\u2090-\u2094\u2102\u2107\u210a-\u2113\u2115\u2119-\u211d\u2124\u2126\u2128\u212a-\u212d\u212f-\u2139\u213c-\u213f\u2145-\u2149\u214e\u2183-\u2184\u2c00-\u2c2e\u2c30-\u2c5e\u2c60-\u2c6f\u2c71-\u2c7d\u2c80-\u2ce4\u2d00-\u2d25\u2d30-\u2d65\u2d6f\u2d80-\u2d96\u2da0-\u2da6\u2da8-\u2dae\u2db0-\u2db6\u2db8-\u2dbe\u2dc0-\u2dc6\u2dc8-\u2dce\u2dd0-\u2dd6\u2dd8-\u2dde\u2e2f\u3005-\u3006\u3031-\u3035\u303b-\u303c\u3041-\u3096\u309d-\u309f\u30a1-\u30fa\u30fc-\u30ff\u3105-\u312d\u3131-\u318e\u31a0-\u31b7\u31f0-\u31ff\u3400-\u4db5\u4e00-\u9fc3\ua000-\ua48c\ua500-\ua60c\ua610-\ua62b\ua640-\ua65f\ua662-\ua66e\ua67f-\ua697\ua717-\ua71f\ua722-\ua788\ua78b-\ua78c\ua7fb-\ua801\ua803-\ua805\ua807-\ua80a\ua80c-\ua822\ua840-\ua873\ua882-\ua8b3\ua8d0-\ua8d9\ua900-\ua925\ua930-\ua946\uaa00-\uaa28\uaa40-\uaa42\uaa44-\uaa4b\uaa50-\uaa59\uac00-\ud7a3\uf900-\ufa2d\ufa30-\ufa6a\ufa70-\ufad9\ufb00-\ufb06\ufb13-\ufb17\ufb1d\ufb1f-\ufb28\ufb2a-\ufb36\ufb38-\ufb3c\ufb3e\ufb40-\ufb41\ufb43-\ufb44\ufb46-\ufbb1\ufbd3-\ufd3d\ufd50-\ufd8f\ufd92-\ufdc7\ufdf0-\ufdfb\ufe33-\ufe34\ufe4d-\ufe4f\ufe70-\ufe74\ufe76-\ufefc\uff10-\uff19\uff21-\uff3a\uff3f\uff41-\uff5a\uff66-\uffbe\uffc2-\uffc7\uffca-\uffcf\uffd2-\uffd7\uffda-\uffdc]/g; + var cp_Q = "Q".charCodeAt(0); + var cp_A = "A".charCodeAt(0); + var cp_Z = "Z".charCodeAt(0); + var dist_Za = "a".charCodeAt(0) - cp_Z - 1; + + asciify = function(text) { + return text.replace(lettersThatJavaScriptDoesNotKnowAndQ, function (m) { + var c = m.charCodeAt(0); + var s = ""; + var v; + while (c > 0) { + v = (c % 51) + cp_A; + if (v >= cp_Q) + v++; + if (v > cp_Z) + v += dist_Za; + s = String.fromCharCode(v) + s; + c = c / 51 | 0; + } + return "Q" + s + "Q"; + }) + }; + + deasciify = function(text) { + return text.replace(/Q([A-PR-Za-z]{1,3})Q/g, function (m, s) { + var c = 0; + var v; + for (var i = 0; i < s.length; i++) { + v = s.charCodeAt(i); + if (v > cp_Z) + v -= dist_Za; + if (v > cp_Q) + v--; + v -= cp_A; + c = (c * 51) + v; + } + return String.fromCharCode(c); + }) + } + })(); + } + + var _DoItalicsAndBold = OPTIONS.asteriskIntraWordEmphasis ? _DoItalicsAndBold_AllowIntrawordWithAsterisk : _DoItalicsAndBoldStrict; + + this.makeHtml = function (text) { + + // + // Main function. The order in which other subs are called here is + // essential. Link and image substitutions need to happen before + // _EscapeSpecialCharsWithinTagAttributes(), so that any *'s or _'s in the + // and tags get encoded. + // + + // This will only happen if makeHtml on the same converter instance is called from a plugin hook. + // Don't do that. + if (g_urls) + throw new Error("Recursive call to converter.makeHtml"); + + // Create the private state objects. + g_urls = new SaveHash(); + g_titles = new SaveHash(); + g_html_blocks = []; + g_list_level = 0; + + text = pluginHooks.preConversion(text); + + // attacklab: Replace ~ with ~T + // This lets us use tilde as an escape char to avoid md5 hashes + // The choice of character is arbitray; anything that isn't + // magic in Markdown will work. + text = text.replace(/~/g, "~T"); + + // attacklab: Replace $ with ~D + // RegExp interprets $ as a special character + // when it's in a replacement string + text = text.replace(/\$/g, "~D"); + + // Standardize line endings + text = text.replace(/\r\n/g, "\n"); // DOS to Unix + text = text.replace(/\r/g, "\n"); // Mac to Unix + + // Make sure text begins and ends with a couple of newlines: + text = "\n\n" + text + "\n\n"; + + // Convert all tabs to spaces. + text = _Detab(text); + + // Strip any lines consisting only of spaces and tabs. + // This makes subsequent regexen easier to write, because we can + // match consecutive blank lines with /\n+/ instead of something + // contorted like /[ \t]*\n+/ . + text = text.replace(/^[ \t]+$/mg, ""); + + text = pluginHooks.postNormalization(text); + + // Turn block-level HTML blocks into hash entries + text = _HashHTMLBlocks(text); + + // Strip link definitions, store in hashes. + text = _StripLinkDefinitions(text); + + text = _RunBlockGamut(text); + + text = _UnescapeSpecialChars(text); + + // attacklab: Restore dollar signs + text = text.replace(/~D/g, "$$"); + + // attacklab: Restore tildes + text = text.replace(/~T/g, "~"); + + text = pluginHooks.postConversion(text); + + g_html_blocks = g_titles = g_urls = null; + + return text; + }; + + function _StripLinkDefinitions(text) { + // + // Strips link definitions from text, stores the URLs and titles in + // hash references. + // + + // Link defs are in the form: ^[id]: url "optional title" + + /* + text = text.replace(/ + ^[ ]{0,3}\[([^\[\]]+)\]: // id = $1 attacklab: g_tab_width - 1 + [ \t]* + \n? // maybe *one* newline + [ \t]* + ? // url = $2 + (?=\s|$) // lookahead for whitespace instead of the lookbehind removed below + [ \t]* + \n? // maybe one newline + [ \t]* + ( // (potential) title = $3 + (\n*) // any lines skipped = $4 attacklab: lookbehind removed + [ \t]+ + ["(] + (.+?) // title = $5 + [")] + [ \t]* + )? // title is optional + (\n+) // subsequent newlines = $6, capturing because they must be put back if the potential title isn't an actual title + /gm, function(){...}); + */ + + text = text.replace(/^[ ]{0,3}\[([^\[\]]+)\]:[ \t]*\n?[ \t]*?(?=\s|$)[ \t]*\n?[ \t]*((\n*)["(](.+?)[")][ \t]*)?(\n+)/gm, + function (wholeMatch, m1, m2, m3, m4, m5, m6) { + m1 = m1.toLowerCase(); + g_urls.set(m1, _EncodeAmpsAndAngles(m2)); // Link IDs are case-insensitive + if (m4) { + // Oops, found blank lines, so it's not a title. + // Put back the parenthetical statement we stole. + return m3 + m6; + } else if (m5) { + g_titles.set(m1, m5.replace(/"/g, """)); + } + + // Completely remove the definition from the text + return ""; + } + ); + + return text; + } + + function _HashHTMLBlocks(text) { + + // Hashify HTML blocks: + // We only want to do this for block-level HTML tags, such as headers, + // lists, and tables. That's because we still want to wrap

    s around + // "paragraphs" that are wrapped in non-block-level tags, such as anchors, + // phrase emphasis, and spans. The list of tags we're looking for is + // hard-coded: + var block_tags_a = "p|div|h[1-6]|blockquote|pre|table|dl|ol|ul|script|noscript|form|fieldset|iframe|math|ins|del" + var block_tags_b = "p|div|h[1-6]|blockquote|pre|table|dl|ol|ul|script|noscript|form|fieldset|iframe|math" + + // First, look for nested blocks, e.g.: + //

    + //
    + // tags for inner block must be indented. + //
    + //
    + // + // The outermost tags must start at the left margin for this to match, and + // the inner nested divs must be indented. + // We need to do this before the next, more liberal match, because the next + // match will start at the first `
    ` and stop at the first `
    `. + + // attacklab: This regex can be expensive when it fails. + + /* + text = text.replace(/ + ( // save in $1 + ^ // start of line (with /m) + <($block_tags_a) // start tag = $2 + \b // word break + // attacklab: hack around khtml/pcre bug... + [^\r]*?\n // any number of lines, minimally matching + // the matching end tag + [ \t]* // trailing spaces/tabs + (?=\n+) // followed by a newline + ) // attacklab: there are sentinel newlines at end of document + /gm,function(){...}}; + */ + text = text.replace(/^(<(p|div|h[1-6]|blockquote|pre|table|dl|ol|ul|script|noscript|form|fieldset|iframe|math|ins|del)\b[^\r]*?\n<\/\2>[ \t]*(?=\n+))/gm, hashMatch); + + // + // Now match more liberally, simply from `\n` to `\n` + // + + /* + text = text.replace(/ + ( // save in $1 + ^ // start of line (with /m) + <($block_tags_b) // start tag = $2 + \b // word break + // attacklab: hack around khtml/pcre bug... + [^\r]*? // any number of lines, minimally matching + .* // the matching end tag + [ \t]* // trailing spaces/tabs + (?=\n+) // followed by a newline + ) // attacklab: there are sentinel newlines at end of document + /gm,function(){...}}; + */ + text = text.replace(/^(<(p|div|h[1-6]|blockquote|pre|table|dl|ol|ul|script|noscript|form|fieldset|iframe|math)\b[^\r]*?.*<\/\2>[ \t]*(?=\n+)\n)/gm, hashMatch); + + // Special case just for
    . It was easier to make a special case than + // to make the other regex more complicated. + + /* + text = text.replace(/ + \n // Starting after a blank line + [ ]{0,3} + ( // save in $1 + (<(hr) // start tag = $2 + \b // word break + ([^<>])*? + \/?>) // the matching end tag + [ \t]* + (?=\n{2,}) // followed by a blank line + ) + /g,hashMatch); + */ + text = text.replace(/\n[ ]{0,3}((<(hr)\b([^<>])*?\/?>)[ \t]*(?=\n{2,}))/g, hashMatch); + + // Special case for standalone HTML comments: + + /* + text = text.replace(/ + \n\n // Starting after a blank line + [ ]{0,3} // attacklab: g_tab_width - 1 + ( // save in $1 + -]|-[^>])(?:[^-]|-[^-])*)--) // see http://www.w3.org/TR/html-markup/syntax.html#comments and http://meta.stackexchange.com/q/95256 + > + [ \t]* + (?=\n{2,}) // followed by a blank line + ) + /g,hashMatch); + */ + text = text.replace(/\n\n[ ]{0,3}(-]|-[^>])(?:[^-]|-[^-])*)--)>[ \t]*(?=\n{2,}))/g, hashMatch); + + // PHP and ASP-style processor instructions ( and <%...%>) + + /* + text = text.replace(/ + (?: + \n\n // Starting after a blank line + ) + ( // save in $1 + [ ]{0,3} // attacklab: g_tab_width - 1 + (?: + <([?%]) // $2 + [^\r]*? + \2> + ) + [ \t]* + (?=\n{2,}) // followed by a blank line + ) + /g,hashMatch); + */ + text = text.replace(/(?:\n\n)([ ]{0,3}(?:<([?%])[^\r]*?\2>)[ \t]*(?=\n{2,}))/g, hashMatch); + + return text; + } + + function hashBlock(text) { + text = text.replace(/(^\n+|\n+$)/g, ""); + // Replace the element text with a marker ("~KxK" where x is its key) + return "\n\n~K" + (g_html_blocks.push(text) - 1) + "K\n\n"; + } + + function hashMatch(wholeMatch, m1) { + return hashBlock(m1); + } + + var blockGamutHookCallback = function (t) { return _RunBlockGamut(t); } + + function _RunBlockGamut(text, doNotUnhash, doNotCreateParagraphs) { + // + // These are all the transformations that form block-level + // tags like paragraphs, headers, and list items. + // + + text = pluginHooks.preBlockGamut(text, blockGamutHookCallback); + + text = _DoHeaders(text); + + // Do Horizontal Rules: + var replacement = "
    \n"; + text = text.replace(/^[ ]{0,2}([ ]?\*[ ]?){3,}[ \t]*$/gm, replacement); + text = text.replace(/^[ ]{0,2}([ ]?-[ ]?){3,}[ \t]*$/gm, replacement); + text = text.replace(/^[ ]{0,2}([ ]?_[ ]?){3,}[ \t]*$/gm, replacement); + + text = _DoLists(text); + text = _DoCodeBlocks(text); + text = _DoBlockQuotes(text); + + text = pluginHooks.postBlockGamut(text, blockGamutHookCallback); + + // We already ran _HashHTMLBlocks() before, in Markdown(), but that + // was to escape raw HTML in the original Markdown source. This time, + // we're escaping the markup we've just created, so that we don't wrap + //

    tags around block-level tags. + text = _HashHTMLBlocks(text); + + text = _FormParagraphs(text, doNotUnhash, doNotCreateParagraphs); + + return text; + } + + function _RunSpanGamut(text) { + // + // These are all the transformations that occur *within* block-level + // tags like paragraphs, headers, and list items. + // + + text = pluginHooks.preSpanGamut(text); + + text = _DoCodeSpans(text); + text = _EscapeSpecialCharsWithinTagAttributes(text); + text = _EncodeBackslashEscapes(text); + + // Process anchor and image tags. Images must come first, + // because ![foo][f] looks like an anchor. + text = _DoImages(text); + text = _DoAnchors(text); + + // Make links out of things like `` + // Must come after _DoAnchors(), because you can use < and > + // delimiters in inline links like [this](). + text = _DoAutoLinks(text); + + text = text.replace(/~P/g, "://"); // put in place to prevent autolinking; reset now + + text = _EncodeAmpsAndAngles(text); + text = _DoItalicsAndBold(text); + + // Do hard breaks: + text = text.replace(/ +\n/g, "
    \n"); + + text = pluginHooks.postSpanGamut(text); + + return text; + } + + function _EscapeSpecialCharsWithinTagAttributes(text) { + // + // Within tags -- meaning between < and > -- encode [\ ` * _] so they + // don't conflict with their use in Markdown for code, italics and strong. + // + + // Build a regex to find HTML tags and comments. See Friedl's + // "Mastering Regular Expressions", 2nd Ed., pp. 200-201. + + // SE: changed the comment part of the regex + + var regex = /(<[a-z\/!$]("[^"]*"|'[^']*'|[^'">])*>|-]|-[^>])(?:[^-]|-[^-])*)--)>)/gi; + + text = text.replace(regex, function (wholeMatch) { + var tag = wholeMatch.replace(/(.)<\/?code>(?=.)/g, "$1`"); + tag = escapeCharacters(tag, wholeMatch.charAt(1) == "!" ? "\\`*_/" : "\\`*_"); // also escape slashes in comments to prevent autolinking there -- http://meta.stackexchange.com/questions/95987 + return tag; + }); + + return text; + } + + function _DoAnchors(text) { + + if (text.indexOf("[") === -1) + return text; + + // + // Turn Markdown link shortcuts into XHTML
    tags. + // + // + // First, handle reference-style links: [link text] [id] + // + + /* + text = text.replace(/ + ( // wrap whole match in $1 + \[ + ( + (?: + \[[^\]]*\] // allow brackets nested one level + | + [^\[] // or anything else + )* + ) + \] + + [ ]? // one optional space + (?:\n[ ]*)? // one optional newline followed by spaces + + \[ + (.*?) // id = $3 + \] + ) + ()()()() // pad remaining backreferences + /g, writeAnchorTag); + */ + text = text.replace(/(\[((?:\[[^\]]*\]|[^\[\]])*)\][ ]?(?:\n[ ]*)?\[(.*?)\])()()()()/g, writeAnchorTag); + + // + // Next, inline-style links: [link text](url "optional title") + // + + /* + text = text.replace(/ + ( // wrap whole match in $1 + \[ + ( + (?: + \[[^\]]*\] // allow brackets nested one level + | + [^\[\]] // or anything else + )* + ) + \] + \( // literal paren + [ \t]* + () // no id, so leave $3 empty + ? + [ \t]* + ( // $5 + (['"]) // quote char = $6 + (.*?) // Title = $7 + \6 // matching quote + [ \t]* // ignore any spaces/tabs between closing quote and ) + )? // title is optional + \) + ) + /g, writeAnchorTag); + */ + + text = text.replace(/(\[((?:\[[^\]]*\]|[^\[\]])*)\]\([ \t]*()?[ \t]*((['"])(.*?)\6[ \t]*)?\))/g, writeAnchorTag); + + // + // Last, handle reference-style shortcuts: [link text] + // These must come last in case you've also got [link test][1] + // or [link test](/foo) + // + + /* + text = text.replace(/ + ( // wrap whole match in $1 + \[ + ([^\[\]]+) // link text = $2; can't contain '[' or ']' + \] + ) + ()()()()() // pad rest of backreferences + /g, writeAnchorTag); + */ + text = text.replace(/(\[([^\[\]]+)\])()()()()()/g, writeAnchorTag); + + return text; + } + + function writeAnchorTag(wholeMatch, m1, m2, m3, m4, m5, m6, m7) { + if (m7 == undefined) m7 = ""; + var whole_match = m1; + var link_text = m2.replace(/:\/\//g, "~P"); // to prevent auto-linking withing the link. will be converted back after the auto-linker runs + var link_id = m3.toLowerCase(); + var url = m4; + var title = m7; + + if (url == "") { + if (link_id == "") { + // lower-case and turn embedded newlines into spaces + link_id = link_text.toLowerCase().replace(/ ?\n/g, " "); + } + url = "#" + link_id; + + if (g_urls.get(link_id) != undefined) { + url = g_urls.get(link_id); + if (g_titles.get(link_id) != undefined) { + title = g_titles.get(link_id); + } + } + else { + if (whole_match.search(/\(\s*\)$/m) > -1) { + // Special case for explicit empty url + url = ""; + } else { + return whole_match; + } + } + } + url = attributeSafeUrl(url); + + var result = ""; + + return result; + } + + function _DoImages(text) { + + if (text.indexOf("![") === -1) + return text; + + // + // Turn Markdown image shortcuts into tags. + // + + // + // First, handle reference-style labeled images: ![alt text][id] + // + + /* + text = text.replace(/ + ( // wrap whole match in $1 + !\[ + (.*?) // alt text = $2 + \] + + [ ]? // one optional space + (?:\n[ ]*)? // one optional newline followed by spaces + + \[ + (.*?) // id = $3 + \] + ) + ()()()() // pad rest of backreferences + /g, writeImageTag); + */ + text = text.replace(/(!\[(.*?)\][ ]?(?:\n[ ]*)?\[(.*?)\])()()()()/g, writeImageTag); + + // + // Next, handle inline images: ![alt text](url "optional title") + // Don't forget: encode * and _ + + /* + text = text.replace(/ + ( // wrap whole match in $1 + !\[ + (.*?) // alt text = $2 + \] + \s? // One optional whitespace character + \( // literal paren + [ \t]* + () // no id, so leave $3 empty + ? // src url = $4 + [ \t]* + ( // $5 + (['"]) // quote char = $6 + (.*?) // title = $7 + \6 // matching quote + [ \t]* + )? // title is optional + \) + ) + /g, writeImageTag); + */ + text = text.replace(/(!\[(.*?)\]\s?\([ \t]*()?[ \t]*((['"])(.*?)\6[ \t]*)?\))/g, writeImageTag); + + return text; + } + + function attributeEncode(text) { + // unconditionally replace angle brackets here -- what ends up in an attribute (e.g. alt or title) + // never makes sense to have verbatim HTML in it (and the sanitizer would totally break it) + return text.replace(/>/g, ">").replace(/" + _RunSpanGamut(m1) + "\n\n"; } + ); + + text = text.replace(/^(.+)[ \t]*\n-+[ \t]*\n+/gm, + function (matchFound, m1) { return "

    " + _RunSpanGamut(m1) + "

    \n\n"; } + ); + + // atx-style headers: + // # Header 1 + // ## Header 2 + // ## Header 2 with closing hashes ## + // ... + // ###### Header 6 + // + + /* + text = text.replace(/ + ^(\#{1,6}) // $1 = string of #'s + [ \t]* + (.+?) // $2 = Header text + [ \t]* + \#* // optional closing #'s (not counted) + \n+ + /gm, function() {...}); + */ + + text = text.replace(/^(\#{1,6})[ \t]*(.+?)[ \t]*\#*\n+/gm, + function (wholeMatch, m1, m2) { + var h_level = m1.length; + return "" + _RunSpanGamut(m2) + "\n\n"; + } + ); + + return text; + } + + function _DoLists(text, isInsideParagraphlessListItem) { + // + // Form HTML ordered (numbered) and unordered (bulleted) lists. + // + + // attacklab: add sentinel to hack around khtml/safari bug: + // http://bugs.webkit.org/show_bug.cgi?id=11231 + text += "~0"; + + // Re-usable pattern to match any entirel ul or ol list: + + /* + var whole_list = / + ( // $1 = whole list + ( // $2 + [ ]{0,3} // attacklab: g_tab_width - 1 + ([*+-]|\d+[.]) // $3 = first list item marker + [ \t]+ + ) + [^\r]+? + ( // $4 + ~0 // sentinel for workaround; should be $ + | + \n{2,} + (?=\S) + (?! // Negative lookahead for another list item marker + [ \t]* + (?:[*+-]|\d+[.])[ \t]+ + ) + ) + ) + /g + */ + var whole_list = /^(([ ]{0,3}([*+-]|\d+[.])[ \t]+)[^\r]+?(~0|\n{2,}(?=\S)(?![ \t]*(?:[*+-]|\d+[.])[ \t]+)))/gm; + if (g_list_level) { + text = text.replace(whole_list, function (wholeMatch, m1, m2) { + var list = m1; + var list_type = (m2.search(/[*+-]/g) > -1) ? "ul" : "ol"; + var first_number; + if (list_type === "ol") + first_number = parseInt(m2, 10) + + var result = _ProcessListItems(list, list_type, isInsideParagraphlessListItem); + + // Trim any trailing whitespace, to put the closing `` + // up on the preceding line, to get it past the current stupid + // HTML block parser. This is a hack to work around the terrible + // hack that is the HTML block parser. + result = result.replace(/\s+$/, ""); + var opening = "<" + list_type; + if (first_number && first_number !== 1) + opening += " start=\"" + first_number + "\""; + result = opening + ">" + result + "\n"; + return result; + }); + } else { + whole_list = /(\n\n|^\n?)(([ ]{0,3}([*+-]|\d+[.])[ \t]+)[^\r]+?(~0|\n{2,}(?=\S)(?![ \t]*(?:[*+-]|\d+[.])[ \t]+)))/g; + text = text.replace(whole_list, function (wholeMatch, m1, m2, m3) { + var runup = m1; + var list = m2; + + var list_type = (m3.search(/[*+-]/g) > -1) ? "ul" : "ol"; + + var first_number; + if (list_type === "ol") + first_number = parseInt(m3, 10) + + var result = _ProcessListItems(list, list_type); + var opening = "<" + list_type; + if (first_number && first_number !== 1) + opening += " start=\"" + first_number + "\""; + + result = runup + opening + ">\n" + result + "\n"; + return result; + }); + } + + // attacklab: strip sentinel + text = text.replace(/~0/, ""); + + return text; + } + + var _listItemMarkers = { ol: "\\d+[.]", ul: "[*+-]" }; + + function _ProcessListItems(list_str, list_type, isInsideParagraphlessListItem) { + // + // Process the contents of a single ordered or unordered list, splitting it + // into individual list items. + // + // list_type is either "ul" or "ol". + + // The $g_list_level global keeps track of when we're inside a list. + // Each time we enter a list, we increment it; when we leave a list, + // we decrement. If it's zero, we're not in a list anymore. + // + // We do this because when we're not inside a list, we want to treat + // something like this: + // + // I recommend upgrading to version + // 8. Oops, now this line is treated + // as a sub-list. + // + // As a single paragraph, despite the fact that the second line starts + // with a digit-period-space sequence. + // + // Whereas when we're inside a list (or sub-list), that line will be + // treated as the start of a sub-list. What a kludge, huh? This is + // an aspect of Markdown's syntax that's hard to parse perfectly + // without resorting to mind-reading. Perhaps the solution is to + // change the syntax rules such that sub-lists must start with a + // starting cardinal number; e.g. "1." or "a.". + + g_list_level++; + + // trim trailing blank lines: + list_str = list_str.replace(/\n{2,}$/, "\n"); + + // attacklab: add sentinel to emulate \z + list_str += "~0"; + + // In the original attacklab showdown, list_type was not given to this function, and anything + // that matched /[*+-]|\d+[.]/ would just create the next
  • , causing this mismatch: + // + // Markdown rendered by WMD rendered by MarkdownSharp + // ------------------------------------------------------------------ + // 1. first 1. first 1. first + // 2. second 2. second 2. second + // - third 3. third * third + // + // We changed this to behave identical to MarkdownSharp. This is the constructed RegEx, + // with {MARKER} being one of \d+[.] or [*+-], depending on list_type: + + /* + list_str = list_str.replace(/ + (^[ \t]*) // leading whitespace = $1 + ({MARKER}) [ \t]+ // list marker = $2 + ([^\r]+? // list item text = $3 + (\n+) + ) + (?= + (~0 | \2 ({MARKER}) [ \t]+) + ) + /gm, function(){...}); + */ + + var marker = _listItemMarkers[list_type]; + var re = new RegExp("(^[ \\t]*)(" + marker + ")[ \\t]+([^\\r]+?(\\n+))(?=(~0|\\1(" + marker + ")[ \\t]+))", "gm"); + var last_item_had_a_double_newline = false; + list_str = list_str.replace(re, + function (wholeMatch, m1, m2, m3) { + var item = m3; + var leading_space = m1; + var ends_with_double_newline = /\n\n$/.test(item); + var contains_double_newline = ends_with_double_newline || item.search(/\n{2,}/) > -1; + + var loose = contains_double_newline || last_item_had_a_double_newline; + item = _RunBlockGamut(_Outdent(item), /* doNotUnhash = */true, /* doNotCreateParagraphs = */ !loose); + + last_item_had_a_double_newline = ends_with_double_newline; + return "
  • " + item + "
  • \n"; + } + ); + + // attacklab: strip sentinel + list_str = list_str.replace(/~0/g, ""); + + g_list_level--; + return list_str; + } + + function _DoCodeBlocks(text) { + + return text; + // + // Process Markdown `
    ` blocks.
    +            //  
    +            /*
    +            text = text.replace(/
    +                (?:\n\n|^)
    +                (                               // $1 = the code block -- one or more lines, starting with a space/tab
    +                    (?:
    +                        (?:[ ]{4}|\t)           // Lines must start with a tab or a tab-width of spaces - attacklab: g_tab_width
    +                        .*\n+
    +                    )+
    +                )
    +                (\n*[ ]{0,3}[^ \t\n]|(?=~0))    // attacklab: g_tab_width
    +            /g ,function(){...});
    +            */
    +
    +            // attacklab: sentinel workarounds for lack of \A and \Z, safari\khtml bug
    +            text += "~0";
    +
    +            text = text.replace(/(?:\n\n|^\n?)((?:(?:[ ]{4}|\t).*\n+)+)(\n*[ ]{0,3}[^ \t\n]|(?=~0))/g,
    +                function (wholeMatch, m1, m2) {
    +                    var codeblock = m1;
    +                    var nextChar = m2;
    +
    +                    codeblock = _EncodeCode(_Outdent(codeblock));
    +                    codeblock = _Detab(codeblock);
    +                    codeblock = codeblock.replace(/^\n+/g, ""); // trim leading newlines
    +                    codeblock = codeblock.replace(/\n+$/g, ""); // trim trailing whitespace
    +
    +                    codeblock = "
    " + codeblock + "\n
    "; + + return "\n\n" + codeblock + "\n\n" + nextChar; + } + ); + + // attacklab: strip sentinel + text = text.replace(/~0/, ""); + + return text; + } + + function _DoCodeSpans(text) { + + return text; + // + // * Backtick quotes are used for spans. + // + // * You can use multiple backticks as the delimiters if you want to + // include literal backticks in the code span. So, this input: + // + // Just type ``foo `bar` baz`` at the prompt. + // + // Will translate to: + // + //

    Just type foo `bar` baz at the prompt.

    + // + // There's no arbitrary limit to the number of backticks you + // can use as delimters. If you need three consecutive backticks + // in your code, use four for delimiters, etc. + // + // * You can use spaces to get literal backticks at the edges: + // + // ... type `` `bar` `` ... + // + // Turns to: + // + // ... type `bar` ... + // + + /* + text = text.replace(/ + (^|[^\\`]) // Character before opening ` can't be a backslash or backtick + (`+) // $2 = Opening run of ` + (?!`) // and no more backticks -- match the full run + ( // $3 = The code block + [^\r]*? + [^`] // attacklab: work around lack of lookbehind + ) + \2 // Matching closer + (?!`) + /gm, function(){...}); + */ + + text = text.replace(/(^|[^\\`])(`+)(?!`)([^\r]*?[^`])\2(?!`)/gm, + function (wholeMatch, m1, m2, m3, m4) { + var c = m3; + //c = c.replace(/^([ \t]*)/g, ""); // leading whitespace + c = c.replace(/[ \t]*$/g, ""); // trailing whitespace + c = _EncodeCode(c); + c = c.replace(/:\/\//g, "~P"); // to prevent auto-linking. Not necessary in code *blocks*, but in code spans. Will be converted back after the auto-linker runs. + return m1 + "" + c + ""; + } + ); + + return text; + } + + function _EncodeCode(text) { + // + // Encode/escape certain characters inside Markdown code runs. + // The point is that in code, these characters are literals, + // and lose their special Markdown meanings. + // + // Encode all ampersands; HTML entities are not + // entities within a Markdown code span. + text = text.replace(/&/g, "&"); + + // Do the angle bracket song and dance: + text = text.replace(//g, ">"); + + // Now, escape characters that are magic in Markdown: + text = escapeCharacters(text, "\*_{}[]\\", false); + + // jj the line above breaks this: + //--- + + //* Item + + // 1. Subitem + + // special char: * + //--- + + return text; + } + + function _DoItalicsAndBoldStrict(text) { + + if (text.indexOf("*") === -1 && text.indexOf("_") === - 1) + return text; + + text = asciify(text); + + // must go first: + + // (^|[\W_]) Start with a non-letter or beginning of string. Store in \1. + // (?:(?!\1)|(?=^)) Either the next character is *not* the same as the previous, + // or we started at the end of the string (in which case the previous + // group had zero width, so we're still there). Because the next + // character is the marker, this means that if there are e.g. multiple + // underscores in a row, we can only match the left-most ones (which + // prevents foo___bar__ from getting bolded) + // (\*|_) The marker character itself, asterisk or underscore. Store in \2. + // \2 The marker again, since bold needs two. + // (?=\S) The first bolded character cannot be a space. + // ([^\r]*?\S) The actual bolded string. At least one character, and it cannot *end* + // with a space either. Note that like in many other places, [^\r] is + // just a workaround for JS' lack of single-line regexes; it's equivalent + // to a . in an /s regex, because the string cannot contain any \r (they + // are removed in the normalizing step). + // \2\2 The marker character, twice -- end of bold. + // (?!\2) Not followed by another marker character (ensuring that we match the + // rightmost two in a longer row)... + // (?=[\W_]|$) ...but by any other non-word character or the end of string. + text = text.replace(/(^|[\W_])(?:(?!\1)|(?=^))(\*|_)\2(?=\S)([^\r]*?\S)\2\2(?!\2)(?=[\W_]|$)/g, + "$1$3"); + + // This is almost identical to the regex, except 1) there's obviously just one marker + // character, and 2) the italicized string cannot contain the marker character. + text = text.replace(/(^|[\W_])(?:(?!\1)|(?=^))(\*|_)(?=\S)((?:(?!\2)[^\r])*?\S)\2(?!\2)(?=[\W_]|$)/g, + "$1$3"); + + return deasciify(text); + } + + function _DoItalicsAndBold_AllowIntrawordWithAsterisk(text) { + + if (text.indexOf("*") === -1 && text.indexOf("_") === - 1) + return text; + + text = asciify(text); + + // must go first: + // (?=[^\r][*_]|[*_]) Optimization only, to find potentially relevant text portions faster. Minimally slower in Chrome, but much faster in IE. + // ( Store in \1. This is the last character before the delimiter + // ^ Either we're at the start of the string (i.e. there is no last character)... + // | ... or we allow one of the following: + // (?= (lookahead; we're not capturing this, just listing legal possibilities) + // \W__ If the delimiter is __, then this last character must be non-word non-underscore (extra-word emphasis only) + // | + // (?!\*)[\W_]\*\* If the delimiter is **, then this last character can be non-word non-asterisk (extra-word emphasis)... + // | + // \w\*\*\w ...or it can be word/underscore, but only if the first bolded character is such a character as well (intra-word emphasis) + // ) + // [^\r] actually capture the character (can't use `.` since it could be \n) + // ) + // (\*\*|__) Store in \2: the actual delimiter + // (?!\2) not followed by the delimiter again (at most one more asterisk/underscore is allowed) + // (?=\S) the first bolded character can't be a space + // ( Store in \3: the bolded string + // + // (?:| Look at all bolded characters except for the last one. Either that's empty, meaning only a single character was bolded... + // [^\r]*? ... otherwise take arbitrary characters, minimally matching; that's all bolded characters except for the last *two* + // (?!\2) the last two characters cannot be the delimiter itself (because that would mean four underscores/asterisks in a row) + // [^\r] capture the next-to-last bolded character + // ) + // (?= lookahead at the very last bolded char and what comes after + // \S_ for underscore-bolding, it can be any non-space + // | + // \w for asterisk-bolding (otherwise the previous alternative would've matched, since \w implies \S), either the last char is word/underscore... + // | + // \S\*\*(?:[\W_]|$) ... or it's any other non-space, but in that case the character *after* the delimiter may not be a word character + // ) + // . actually capture the last character (can use `.` this time because the lookahead ensures \S in all cases) + // ) + // (?= lookahead; list the legal possibilities for the closing delimiter and its following character + // __(?:\W|$) for underscore-bolding, the following character (if any) must be non-word non-underscore + // | + // \*\*(?:[^*]|$) for asterisk-bolding, any non-asterisk is allowed (note we already ensured above that it's not a word character if the last bolded character wasn't one) + // ) + // \2 actually capture the closing delimiter (and make sure that it matches the opening one) + + text = text.replace(/(?=[^\r][*_]|[*_])(^|(?=\W__|(?!\*)[\W_]\*\*|\w\*\*\w)[^\r])(\*\*|__)(?!\2)(?=\S)((?:|[^\r]*?(?!\2)[^\r])(?=\S_|\w|\S\*\*(?:[\W_]|$)).)(?=__(?:\W|$)|\*\*(?:[^*]|$))\2/g, + "$1$3"); + + // now : + // (?=[^\r][*_]|[*_]) Optimization, see above. + // ( Store in \1. This is the last character before the delimiter + // ^ Either we're at the start of the string (i.e. there is no last character)... + // | ... or we allow one of the following: + // (?= (lookahead; we're not capturing this, just listing legal possibilities) + // \W_ If the delimiter is _, then this last character must be non-word non-underscore (extra-word emphasis only) + // | + // (?!\*) otherwise, we list two possiblities for * as the delimiter; in either case, the last characters cannot be an asterisk itself + // (?: + // [\W_]\* this last character can be non-word (extra-word emphasis)... + // | + // \D\*(?=\w)\D ...or it can be word (otherwise the first alternative would've matched), but only if + // a) the first italicized character is such a character as well (intra-word emphasis), and + // b) neither character on either side of the asterisk is a digit + // ) + // ) + // [^\r] actually capture the character (can't use `.` since it could be \n) + // ) + // (\*|_) Store in \2: the actual delimiter + // (?!\2\2\2) not followed by more than two more instances of the delimiter + // (?=\S) the first italicized character can't be a space + // ( Store in \3: the italicized string + // (?:(?!\2)[^\r])*? arbitrary characters except for the delimiter itself, minimally matching + // (?= lookahead at the very last italicized char and what comes after + // [^\s_]_ for underscore-italicizing, it can be any non-space non-underscore + // | + // (?=\w)\D\*\D for asterisk-italicizing, either the last char is word/underscore *and* neither character on either side of the asterisk is a digit... + // | + // [^\s*]\*(?:[\W_]|$) ... or that last char is any other non-space non-asterisk, but then the character after the delimiter (if any) must be non-word + // ) + // . actually capture the last character (can use `.` this time because the lookahead ensures \S in all cases) + // ) + // (?= lookahead; list the legal possibilities for the closing delimiter and its following character + // _(?:\W|$) for underscore-italicizing, the following character (if any) must be non-word non-underscore + // | + // \*(?:[^*]|$) for asterisk-italicizing, any non-asterisk is allowed; all other restrictions have already been ensured in the previous lookahead + // ) + // \2 actually capture the closing delimiter (and make sure that it matches the opening one) + + text = text.replace(/(?=[^\r][*_]|[*_])(^|(?=\W_|(?!\*)(?:[\W_]\*|\D\*(?=\w)\D))[^\r])(\*|_)(?!\2\2\2)(?=\S)((?:(?!\2)[^\r])*?(?=[^\s_]_|(?=\w)\D\*\D|[^\s*]\*(?:[\W_]|$)).)(?=_(?:\W|$)|\*(?:[^*]|$))\2/g, + "$1$3"); + + return deasciify(text); + } + + + function _DoBlockQuotes(text) { + + /* + text = text.replace(/ + ( // Wrap whole match in $1 + ( + ^[ \t]*>[ \t]? // '>' at the start of a line + .+\n // rest of the first line + (.+\n)* // subsequent consecutive lines + \n* // blanks + )+ + ) + /gm, function(){...}); + */ + + text = text.replace(/((^[ \t]*>[ \t]?.+\n(.+\n)*\n*)+)/gm, + function (wholeMatch, m1) { + var bq = m1; + + // attacklab: hack around Konqueror 3.5.4 bug: + // "----------bug".replace(/^-/g,"") == "bug" + + bq = bq.replace(/^[ \t]*>[ \t]?/gm, "~0"); // trim one level of quoting + + // attacklab: clean up hack + bq = bq.replace(/~0/g, ""); + + bq = bq.replace(/^[ \t]+$/gm, ""); // trim whitespace-only lines + bq = _RunBlockGamut(bq); // recurse + + bq = bq.replace(/(^|\n)/g, "$1 "); + // These leading spaces screw with
     content, so we need to fix that:
    +                    bq = bq.replace(
    +                            /(\s*
    [^\r]+?<\/pre>)/gm,
    +                        function (wholeMatch, m1) {
    +                            var pre = m1;
    +                            // attacklab: hack around Konqueror 3.5.4 bug:
    +                            pre = pre.replace(/^  /mg, "~0");
    +                            pre = pre.replace(/~0/g, "");
    +                            return pre;
    +                        });
    +
    +                    return hashBlock("
    \n" + bq + "\n
    "); + } + ); + return text; + } + + function _FormParagraphs(text, doNotUnhash, doNotCreateParagraphs) { + // + // Params: + // $text - string to process with html

    tags + // + + + // MODIFIED TO ALLOW LEADING SPACES + text = text.replace(/ /g, '\u00a0'); + // Strip leading and trailing lines: + text = text.replace(/^\n+/g, ""); + text = text.replace(/\n+$/g, ""); + + var grafs = text.split(/\n{2,}/g); + var grafsOut = []; + + var markerRe = /~K(\d+)K/; + + // + // Wrap

    tags. + // + var end = grafs.length; + for (var i = 0; i < end; i++) { + var str = grafs[i]; + + // if this is an HTML marker, copy it + if (markerRe.test(str)) { + grafsOut.push(str); + } + else if (/\S/.test(str)) { + str = _RunSpanGamut(str); + str = str.replace(/^([ \t]*)/g, doNotCreateParagraphs ? "" : "

    "); + if (!doNotCreateParagraphs) + str += "

    " + grafsOut.push(str); + } + + } + // + // Unhashify HTML blocks + // + if (!doNotUnhash) { + end = grafsOut.length; + for (var i = 0; i < end; i++) { + var foundAny = true; + while (foundAny) { // we may need several runs, since the data may be nested + foundAny = false; + grafsOut[i] = grafsOut[i].replace(/~K(\d+)K/g, function (wholeMatch, id) { + foundAny = true; + return g_html_blocks[id]; + }); + } + } + } + return grafsOut.join("\n\n"); + } + + function _EncodeAmpsAndAngles(text) { + // Smart processing for ampersands and angle brackets that need to be encoded. + + // Ampersand-encoding based entirely on Nat Irons's Amputator MT plugin: + // http://bumppo.net/projects/amputator/ + text = text.replace(/&(?!#?[xX]?(?:[0-9a-fA-F]+|\w+);)/g, "&"); + + // Encode naked <'s + text = text.replace(/<(?![a-z\/?!]|~D)/gi, "<"); + + return text; + } + + function _EncodeBackslashEscapes(text) { + // + // Parameter: String. + // Returns: The string, with after processing the following backslash + // escape sequences. + // + + // attacklab: The polite way to do this is with the new + // escapeCharacters() function: + // + // text = escapeCharacters(text,"\\",true); + // text = escapeCharacters(text,"`*_{}[]()>#+-.!",true); + // + // ...but we're sidestepping its use of the (slow) RegExp constructor + // as an optimization for Firefox. This function gets called a LOT. + + text = text.replace(/\\(\\)/g, escapeCharacters_callback); + text = text.replace(/\\([`*_{}\[\]()>#+-.!])/g, escapeCharacters_callback); + return text; + } + + var charInsideUrl = "[-A-Z0-9+&@#/%?=~_|[\\]()!:,.;]", + charEndingUrl = "[-A-Z0-9+&@#/%=~_|[\\])]", + autoLinkRegex = new RegExp("(=\"|<)?\\b(https?|ftp)(://" + charInsideUrl + "*" + charEndingUrl + ")(?=$|\\W)", "gi"), + endCharRegex = new RegExp(charEndingUrl, "i"); + + function handleTrailingParens(wholeMatch, lookbehind, protocol, link) { + if (lookbehind) + return wholeMatch; + if (link.charAt(link.length - 1) !== ")") + return "<" + protocol + link + ">"; + var parens = link.match(/[()]/g); + var level = 0; + for (var i = 0; i < parens.length; i++) { + if (parens[i] === "(") { + if (level <= 0) + level = 1; + else + level++; + } + else { + level--; + } + } + var tail = ""; + if (level < 0) { + var re = new RegExp("\\){1," + (-level) + "}$"); + link = link.replace(re, function (trailingParens) { + tail = trailingParens; + return ""; + }); + } + if (tail) { + var lastChar = link.charAt(link.length - 1); + if (!endCharRegex.test(lastChar)) { + tail = lastChar + tail; + link = link.substr(0, link.length - 1); + } + } + return "<" + protocol + link + ">" + tail; + } + + function _DoAutoLinks(text) { + + // note that at this point, all other URL in the text are already hyperlinked as
    + // *except* for the case + + // automatically add < and > around unadorned raw hyperlinks + // must be preceded by a non-word character (and not by =" or <) and followed by non-word/EOF character + // simulating the lookbehind in a consuming way is okay here, since a URL can neither and with a " nor + // with a <, so there is no risk of overlapping matches. + text = text.replace(autoLinkRegex, handleTrailingParens); + + // autolink anything like + + + var replacer = function (wholematch, m1) { + var url = attributeSafeUrl(m1); + + return "" + pluginHooks.plainLinkText(m1) + ""; + }; + text = text.replace(/<((https?|ftp):[^'">\s]+)>/gi, replacer); + + // Email addresses: + /* + text = text.replace(/ + < + (?:mailto:)? + ( + [-.\w]+ + \@ + [-a-z0-9]+(\.[-a-z0-9]+)*\.[a-z]+ + ) + > + /gi, _DoAutoLinks_callback()); + */ + + /* disabling email autolinking, since we don't do that on the server, either + text = text.replace(/<(?:mailto:)?([-.\w]+\@[-a-z0-9]+(\.[-a-z0-9]+)*\.[a-z]+)>/gi, + function(wholeMatch,m1) { + return _EncodeEmailAddress( _UnescapeSpecialChars(m1) ); + } + ); + */ + return text; + } + + function _UnescapeSpecialChars(text) { + // + // Swap back in all the special characters we've hidden. + // + text = text.replace(/~E(\d+)E/g, + function (wholeMatch, m1) { + var charCodeToReplace = parseInt(m1); + return String.fromCharCode(charCodeToReplace); + } + ); + return text; + } + + function _Outdent(text) { + // + // Remove one level of line-leading tabs or spaces + // + + // attacklab: hack around Konqueror 3.5.4 bug: + // "----------bug".replace(/^-/g,"") == "bug" + + text = text.replace(/^(\t|[ ]{1,4})/gm, "~0"); // attacklab: g_tab_width + + // attacklab: clean up hack + text = text.replace(/~0/g, "") + + return text; + } + + function _Detab(text) { + if (!/\t/.test(text)) + return text; + + var spaces = [" ", " ", " ", " "], + skew = 0, + v; + + return text.replace(/[\n\t]/g, function (match, offset) { + if (match === "\n") { + skew = offset + 1; + return match; + } + v = (offset - skew) % 4; + skew = offset + 1; + return spaces[v]; + }); + } + + // + // attacklab: Utility functions + // + + function attributeSafeUrl(url) { + url = attributeEncode(url); + url = escapeCharacters(url, "*_:()[]") + return url; + } + + function escapeCharacters(text, charsToEscape, afterBackslash) { + // First we have to escape the escape characters so that + // we can build a character class out of them + var regexString = "([" + charsToEscape.replace(/([\[\]\\])/g, "\\$1") + "])"; + + if (afterBackslash) { + regexString = "\\\\" + regexString; + } + + var regex = new RegExp(regexString, "g"); + text = text.replace(regex, escapeCharacters_callback); + + return text; + } + + + function escapeCharacters_callback(wholeMatch, m1) { + var charCodeToEscape = m1.charCodeAt(0); + return "~E" + charCodeToEscape + "E"; + } + + }; // end of the Markdown.Converter constructor + +})(); \ No newline at end of file diff --git a/resources/js/pagedown/Markdown.Editor.js b/resources/js/pagedown/Markdown.Editor.js new file mode 100644 index 0000000000..b15cd05aa8 --- /dev/null +++ b/resources/js/pagedown/Markdown.Editor.js @@ -0,0 +1,2301 @@ +// needs Markdown.Converter.js at the moment + +(function () { + + var util = {}, + position = {}, + ui = {}, + doc = window.document, + re = window.RegExp, + nav = window.navigator, + SETTINGS = { lineLength: 72 }, + + // Used to work around some browser bugs where we can't use feature testing. + uaSniffed = { + isIE: /msie/.test(nav.userAgent.toLowerCase()), + isIE_5or6: /msie 6/.test(nav.userAgent.toLowerCase()) || /msie 5/.test(nav.userAgent.toLowerCase()), + isOpera: /opera/.test(nav.userAgent.toLowerCase()) + }; + + var defaultsStrings = { + bold: "Strong Ctrl+B", + boldexample: "strong text", + + italic: "Emphasis Ctrl+I", + italicexample: "emphasized text", + + link: "Hyperlink Ctrl+L", + linkdescription: "enter link description here", + linkdialog: "

    Insert Hyperlink

    http://example.com/ \"optional title\"

    ", + + quote: "Blockquote
    Ctrl+Q", + quoteexample: "Blockquote", + + code: "Code Sample
     Ctrl+K",
    +        codeexample: "enter code here",
    +
    +        image: "Image  Ctrl+G",
    +        imagedescription: "enter image description here",
    +        imagedialog: "

    Insert Image

    http://example.com/images/diagram.jpg \"optional title\"

    Need
    free image hosting?

    ", + + olist: "Numbered List
      Ctrl+O", + ulist: "Bulleted List
        Ctrl+U", + litem: "List item", + + heading: "Heading

        /

        Ctrl+H", + headingexample: "Heading", + + hr: "Horizontal Rule
        Ctrl+R", + + undo: "Undo - Ctrl+Z", + redo: "Redo - Ctrl+Y", + redomac: "Redo - Ctrl+Shift+Z", + + help: "Markdown Editing Help" + }; + + + // ------------------------------------------------------------------- + // YOUR CHANGES GO HERE + // + // I've tried to localize the things you are likely to change to + // this area. + // ------------------------------------------------------------------- + + // The default text that appears in the dialog input box when entering + // links. + var imageDefaultText = "http://"; + var linkDefaultText = "http://"; + + // ------------------------------------------------------------------- + // END OF YOUR CHANGES + // ------------------------------------------------------------------- + + // options, if given, can have the following properties: + // options.helpButton = { handler: yourEventHandler } + // options.strings = { italicexample: "slanted text" } + // `yourEventHandler` is the click handler for the help button. + // If `options.helpButton` isn't given, not help button is created. + // `options.strings` can have any or all of the same properties as + // `defaultStrings` above, so you can just override some string displayed + // to the user on a case-by-case basis, or translate all strings to + // a different language. + // + // For backwards compatibility reasons, the `options` argument can also + // be just the `helpButton` object, and `strings.help` can also be set via + // `helpButton.title`. This should be considered legacy. + // + // The constructed editor object has the methods: + // - getConverter() returns the markdown converter object that was passed to the constructor + // - run() actually starts the editor; should be called after all necessary plugins are registered. Calling this more than once is a no-op. + // - refreshPreview() forces the preview to be updated. This method is only available after run() was called. + Markdown.Editor = function (markdownConverter, idPostfix, options) { + + options = options || {}; + + if (typeof options.handler === "function") { //backwards compatible behavior + options = { helpButton: options }; + } + options.strings = options.strings || {}; + if (options.helpButton) { + options.strings.help = options.strings.help || options.helpButton.title; + } + var getString = function (identifier) { return options.strings[identifier] || defaultsStrings[identifier]; } + + idPostfix = idPostfix || ""; + + var hooks = this.hooks = new Markdown.HookCollection(); + hooks.addNoop("onPreviewRefresh"); // called with no arguments after the preview has been refreshed + hooks.addNoop("postBlockquoteCreation"); // called with the user's selection *after* the blockquote was created; should return the actual to-be-inserted text + hooks.addFalse("insertImageDialog"); /* called with one parameter: a callback to be called with the URL of the image. If the application creates + * its own image insertion dialog, this hook should return true, and the callback should be called with the chosen + * image url (or null if the user cancelled). If this hook returns false, the default dialog will be used. + */ + + this.getConverter = function () { return markdownConverter; } + + var that = this, + panels; + + this.run = function () { + if (panels) + return; // already initialized + + panels = new PanelCollection(idPostfix); + var commandManager = new CommandManager(hooks, getString, markdownConverter); + var previewManager = new PreviewManager(markdownConverter, panels, function () { hooks.onPreviewRefresh(); }); + var undoManager, uiManager; + + if (!/\?noundo/.test(doc.location.href)) { + undoManager = new UndoManager(function () { + previewManager.refresh(); + if (uiManager) // not available on the first call + uiManager.setUndoRedoButtonStates(); + }, panels); + this.textOperation = function (f) { + undoManager.setCommandMode(); + f(); + that.refreshPreview(); + } + } + + uiManager = new UIManager(idPostfix, panels, undoManager, previewManager, commandManager, options.helpButton, getString); + uiManager.setUndoRedoButtonStates(); + + var forceRefresh = that.refreshPreview = function () { previewManager.refresh(true); }; + + forceRefresh(); + }; + + } + + // before: contains all the text in the input box BEFORE the selection. + // after: contains all the text in the input box AFTER the selection. + function Chunks() { } + + // startRegex: a regular expression to find the start tag + // endRegex: a regular expresssion to find the end tag + Chunks.prototype.findTags = function (startRegex, endRegex) { + + var chunkObj = this; + var regex; + + if (startRegex) { + + regex = util.extendRegExp(startRegex, "", "$"); + + this.before = this.before.replace(regex, + function (match) { + chunkObj.startTag = chunkObj.startTag + match; + return ""; + }); + + regex = util.extendRegExp(startRegex, "^", ""); + + this.selection = this.selection.replace(regex, + function (match) { + chunkObj.startTag = chunkObj.startTag + match; + return ""; + }); + } + + if (endRegex) { + + regex = util.extendRegExp(endRegex, "", "$"); + + this.selection = this.selection.replace(regex, + function (match) { + chunkObj.endTag = match + chunkObj.endTag; + return ""; + }); + + regex = util.extendRegExp(endRegex, "^", ""); + + this.after = this.after.replace(regex, + function (match) { + chunkObj.endTag = match + chunkObj.endTag; + return ""; + }); + } + }; + + // If remove is false, the whitespace is transferred + // to the before/after regions. + // + // If remove is true, the whitespace disappears. + Chunks.prototype.trimWhitespace = function (remove) { + var beforeReplacer, afterReplacer, that = this; + if (remove) { + beforeReplacer = afterReplacer = ""; + } else { + beforeReplacer = function (s) { that.before += s; return ""; } + afterReplacer = function (s) { that.after = s + that.after; return ""; } + } + + this.selection = this.selection.replace(/^(\s*)/, beforeReplacer).replace(/(\s*)$/, afterReplacer); + }; + + + Chunks.prototype.skipLines = function (nLinesBefore, nLinesAfter, findExtraNewlines) { + + if (nLinesBefore === undefined) { + nLinesBefore = 1; + } + + if (nLinesAfter === undefined) { + nLinesAfter = 1; + } + + nLinesBefore++; + nLinesAfter++; + + var regexText; + var replacementText; + + // chrome bug ... documented at: http://meta.stackexchange.com/questions/63307/blockquote-glitch-in-editor-in-chrome-6-and-7/65985#65985 + if (navigator.userAgent.match(/Chrome/)) { + "X".match(/()./); + } + + this.selection = this.selection.replace(/(^\n*)/, ""); + + this.startTag = this.startTag + re.$1; + + this.selection = this.selection.replace(/(\n*$)/, ""); + this.endTag = this.endTag + re.$1; + this.startTag = this.startTag.replace(/(^\n*)/, ""); + this.before = this.before + re.$1; + this.endTag = this.endTag.replace(/(\n*$)/, ""); + this.after = this.after + re.$1; + + if (this.before) { + + regexText = replacementText = ""; + + while (nLinesBefore--) { + regexText += "\\n?"; + replacementText += "\n"; + } + + if (findExtraNewlines) { + regexText = "\\n*"; + } + this.before = this.before.replace(new re(regexText + "$", ""), replacementText); + } + + if (this.after) { + + regexText = replacementText = ""; + + while (nLinesAfter--) { + regexText += "\\n?"; + replacementText += "\n"; + } + if (findExtraNewlines) { + regexText = "\\n*"; + } + + this.after = this.after.replace(new re(regexText, ""), replacementText); + } + }; + + // end of Chunks + + // A collection of the important regions on the page. + // Cached so we don't have to keep traversing the DOM. + // Also holds ieCachedRange and ieCachedScrollTop, where necessary; working around + // this issue: + // Internet explorer has problems with CSS sprite buttons that use HTML + // lists. When you click on the background image "button", IE will + // select the non-existent link text and discard the selection in the + // textarea. The solution to this is to cache the textarea selection + // on the button's mousedown event and set a flag. In the part of the + // code where we need to grab the selection, we check for the flag + // and, if it's set, use the cached area instead of querying the + // textarea. + // + // This ONLY affects Internet Explorer (tested on versions 6, 7 + // and 8) and ONLY on button clicks. Keyboard shortcuts work + // normally since the focus never leaves the textarea. + function PanelCollection(postfix) { + this.buttonBar = doc.getElementById("wmd-button-bar" + postfix); + this.preview = doc.getElementById("wmd-preview" + postfix); + this.input = doc.getElementById("wmd-input" + postfix); + }; + + // Returns true if the DOM element is visible, false if it's hidden. + // Checks if display is anything other than none. + util.isVisible = function (elem) { + + if (window.getComputedStyle) { + // Most browsers + return window.getComputedStyle(elem, null).getPropertyValue("display") !== "none"; + } + else if (elem.currentStyle) { + // IE + return elem.currentStyle["display"] !== "none"; + } + }; + + + // Adds a listener callback to a DOM element which is fired on a specified + // event. + util.addEvent = function (elem, event, listener) { + if (elem.attachEvent) { + // IE only. The "on" is mandatory. + elem.attachEvent("on" + event, listener); + } + else { + // Other browsers. + elem.addEventListener(event, listener, false); + } + }; + + + // Removes a listener callback from a DOM element which is fired on a specified + // event. + util.removeEvent = function (elem, event, listener) { + if (elem.detachEvent) { + // IE only. The "on" is mandatory. + elem.detachEvent("on" + event, listener); + } + else { + // Other browsers. + elem.removeEventListener(event, listener, false); + } + }; + + // Converts \r\n and \r to \n. + util.fixEolChars = function (text) { + text = text.replace(/\r\n/g, "\n"); + text = text.replace(/\r/g, "\n"); + return text; + }; + + // Extends a regular expression. Returns a new RegExp + // using pre + regex + post as the expression. + // Used in a few functions where we have a base + // expression and we want to pre- or append some + // conditions to it (e.g. adding "$" to the end). + // The flags are unchanged. + // + // regex is a RegExp, pre and post are strings. + util.extendRegExp = function (regex, pre, post) { + + if (pre === null || pre === undefined) { + pre = ""; + } + if (post === null || post === undefined) { + post = ""; + } + + var pattern = regex.toString(); + var flags; + + // Replace the flags with empty space and store them. + pattern = pattern.replace(/\/([gim]*)$/, function (wholeMatch, flagsPart) { + flags = flagsPart; + return ""; + }); + + // Remove the slash delimiters on the regular expression. + pattern = pattern.replace(/(^\/|\/$)/g, ""); + pattern = pre + pattern + post; + + return new re(pattern, flags); + } + + // UNFINISHED + // The assignment in the while loop makes jslint cranky. + // I'll change it to a better loop later. + position.getTop = function (elem, isInner) { + var result = elem.offsetTop; + if (!isInner) { + while (elem = elem.offsetParent) { + result += elem.offsetTop; + } + } + return result; + }; + + position.getHeight = function (elem) { + return elem.offsetHeight || elem.scrollHeight; + }; + + position.getWidth = function (elem) { + return elem.offsetWidth || elem.scrollWidth; + }; + + position.getPageSize = function () { + + var scrollWidth, scrollHeight; + var innerWidth, innerHeight; + + // It's not very clear which blocks work with which browsers. + if (self.innerHeight && self.scrollMaxY) { + scrollWidth = doc.body.scrollWidth; + scrollHeight = self.innerHeight + self.scrollMaxY; + } + else if (doc.body.scrollHeight > doc.body.offsetHeight) { + scrollWidth = doc.body.scrollWidth; + scrollHeight = doc.body.scrollHeight; + } + else { + scrollWidth = doc.body.offsetWidth; + scrollHeight = doc.body.offsetHeight; + } + + if (self.innerHeight) { + // Non-IE browser + innerWidth = self.innerWidth; + innerHeight = self.innerHeight; + } + else if (doc.documentElement && doc.documentElement.clientHeight) { + // Some versions of IE (IE 6 w/ a DOCTYPE declaration) + innerWidth = doc.documentElement.clientWidth; + innerHeight = doc.documentElement.clientHeight; + } + else if (doc.body) { + // Other versions of IE + innerWidth = doc.body.clientWidth; + innerHeight = doc.body.clientHeight; + } + + var maxWidth = Math.max(scrollWidth, innerWidth); + var maxHeight = Math.max(scrollHeight, innerHeight); + return [maxWidth, maxHeight, innerWidth, innerHeight]; + }; + + // Handles pushing and popping TextareaStates for undo/redo commands. + // I should rename the stack variables to list. + function UndoManager(callback, panels) { + + var undoObj = this; + var undoStack = []; // A stack of undo states + var stackPtr = 0; // The index of the current state + var mode = "none"; + var lastState; // The last state + var timer; // The setTimeout handle for cancelling the timer + var inputStateObj; + + // Set the mode for later logic steps. + var setMode = function (newMode, noSave) { + if (mode != newMode) { + mode = newMode; + if (!noSave) { + saveState(); + } + } + + if (!uaSniffed.isIE || mode != "moving") { + timer = setTimeout(refreshState, 1); + } + else { + inputStateObj = null; + } + }; + + var refreshState = function (isInitialState) { + inputStateObj = new TextareaState(panels, isInitialState); + timer = undefined; + }; + + this.setCommandMode = function () { + mode = "command"; + saveState(); + timer = setTimeout(refreshState, 0); + }; + + this.canUndo = function () { + return stackPtr > 1; + }; + + this.canRedo = function () { + if (undoStack[stackPtr + 1]) { + return true; + } + return false; + }; + + // Removes the last state and restores it. + this.undo = function () { + + if (undoObj.canUndo()) { + if (lastState) { + // What about setting state -1 to null or checking for undefined? + lastState.restore(); + lastState = null; + } + else { + undoStack[stackPtr] = new TextareaState(panels); + undoStack[--stackPtr].restore(); + + if (callback) { + callback(); + } + } + } + + mode = "none"; + panels.input.focus(); + refreshState(); + }; + + // Redo an action. + this.redo = function () { + + if (undoObj.canRedo()) { + + undoStack[++stackPtr].restore(); + + if (callback) { + callback(); + } + } + + mode = "none"; + panels.input.focus(); + refreshState(); + }; + + // Push the input area state to the stack. + var saveState = function () { + var currState = inputStateObj || new TextareaState(panels); + + if (!currState) { + return false; + } + if (mode == "moving") { + if (!lastState) { + lastState = currState; + } + return; + } + if (lastState) { + if (undoStack[stackPtr - 1].text != lastState.text) { + undoStack[stackPtr++] = lastState; + } + lastState = null; + } + undoStack[stackPtr++] = currState; + undoStack[stackPtr + 1] = null; + if (callback) { + callback(); + } + }; + + var handleCtrlYZ = function (event) { + + var handled = false; + + if ((event.ctrlKey || event.metaKey) && !event.altKey) { + + // IE and Opera do not support charCode. + var keyCode = event.charCode || event.keyCode; + var keyCodeChar = String.fromCharCode(keyCode); + + switch (keyCodeChar.toLowerCase()) { + + case "y": + undoObj.redo(); + handled = true; + break; + + case "z": + if (!event.shiftKey) { + undoObj.undo(); + } + else { + undoObj.redo(); + } + handled = true; + break; + } + } + + if (handled) { + if (event.preventDefault) { + event.preventDefault(); + } + if (window.event) { + window.event.returnValue = false; + } + return; + } + }; + + // Set the mode depending on what is going on in the input area. + var handleModeChange = function (event) { + + if (!event.ctrlKey && !event.metaKey) { + + var keyCode = event.keyCode; + + if ((keyCode >= 33 && keyCode <= 40) || (keyCode >= 63232 && keyCode <= 63235)) { + // 33 - 40: page up/dn and arrow keys + // 63232 - 63235: page up/dn and arrow keys on safari + setMode("moving"); + } + else if (keyCode == 8 || keyCode == 46 || keyCode == 127) { + // 8: backspace + // 46: delete + // 127: delete + setMode("deleting"); + } + else if (keyCode == 13) { + // 13: Enter + setMode("newlines"); + } + else if (keyCode == 27) { + // 27: escape + setMode("escape"); + } + else if ((keyCode < 16 || keyCode > 20) && keyCode != 91) { + // 16-20 are shift, etc. + // 91: left window key + // I think this might be a little messed up since there are + // a lot of nonprinting keys above 20. + setMode("typing"); + } + } + }; + + var setEventHandlers = function () { + util.addEvent(panels.input, "keypress", function (event) { + // keyCode 89: y + // keyCode 90: z + if ((event.ctrlKey || event.metaKey) && !event.altKey && (event.keyCode == 89 || event.keyCode == 90)) { + event.preventDefault(); + } + }); + + var handlePaste = function () { + if (uaSniffed.isIE || (inputStateObj && inputStateObj.text != panels.input.value)) { + if (timer == undefined) { + mode = "paste"; + saveState(); + refreshState(); + } + } + }; + + util.addEvent(panels.input, "keydown", handleCtrlYZ); + util.addEvent(panels.input, "keydown", handleModeChange); + util.addEvent(panels.input, "mousedown", function () { + setMode("moving"); + }); + + panels.input.onpaste = handlePaste; + panels.input.ondrop = handlePaste; + }; + + var init = function () { + setEventHandlers(); + refreshState(true); + saveState(); + }; + + init(); + } + + // end of UndoManager + + // The input textarea state/contents. + // This is used to implement undo/redo by the undo manager. + function TextareaState(panels, isInitialState) { + + // Aliases + var stateObj = this; + var inputArea = panels.input; + this.init = function () { + if (!util.isVisible(inputArea)) { + return; + } + if (!isInitialState && doc.activeElement && doc.activeElement !== inputArea) { // this happens when tabbing out of the input box + return; + } + + this.setInputAreaSelectionStartEnd(); + this.scrollTop = inputArea.scrollTop; + if (!this.text && inputArea.selectionStart || inputArea.selectionStart === 0) { + this.text = inputArea.value; + } + + } + + // Sets the selected text in the input box after we've performed an + // operation. + this.setInputAreaSelection = function () { + + if (!util.isVisible(inputArea)) { + return; + } + + if (inputArea.selectionStart !== undefined && !uaSniffed.isOpera) { + + inputArea.focus(); + inputArea.selectionStart = stateObj.start; + inputArea.selectionEnd = stateObj.end; + inputArea.scrollTop = stateObj.scrollTop; + } + else if (doc.selection) { + + if (doc.activeElement && doc.activeElement !== inputArea) { + return; + } + + inputArea.focus(); + var range = inputArea.createTextRange(); + range.moveStart("character", -inputArea.value.length); + range.moveEnd("character", -inputArea.value.length); + range.moveEnd("character", stateObj.end); + range.moveStart("character", stateObj.start); + range.select(); + } + }; + + this.setInputAreaSelectionStartEnd = function () { + + if (!panels.ieCachedRange && (inputArea.selectionStart || inputArea.selectionStart === 0)) { + + stateObj.start = inputArea.selectionStart; + stateObj.end = inputArea.selectionEnd; + } + else if (doc.selection) { + + stateObj.text = util.fixEolChars(inputArea.value); + + // IE loses the selection in the textarea when buttons are + // clicked. On IE we cache the selection. Here, if something is cached, + // we take it. + var range = panels.ieCachedRange || doc.selection.createRange(); + + var fixedRange = util.fixEolChars(range.text); + var marker = "\x07"; + var markedRange = marker + fixedRange + marker; + range.text = markedRange; + var inputText = util.fixEolChars(inputArea.value); + + range.moveStart("character", -markedRange.length); + range.text = fixedRange; + + stateObj.start = inputText.indexOf(marker); + stateObj.end = inputText.lastIndexOf(marker) - marker.length; + + var len = stateObj.text.length - util.fixEolChars(inputArea.value).length; + + if (len) { + range.moveStart("character", -fixedRange.length); + while (len--) { + fixedRange += "\n"; + stateObj.end += 1; + } + range.text = fixedRange; + } + + if (panels.ieCachedRange) + stateObj.scrollTop = panels.ieCachedScrollTop; // this is set alongside with ieCachedRange + + panels.ieCachedRange = null; + + this.setInputAreaSelection(); + } + }; + + // Restore this state into the input area. + this.restore = function () { + + if (stateObj.text != undefined && stateObj.text != inputArea.value) { + inputArea.value = stateObj.text; + } + this.setInputAreaSelection(); + inputArea.scrollTop = stateObj.scrollTop; + }; + + // Gets a collection of HTML chunks from the inptut textarea. + this.getChunks = function () { + + var chunk = new Chunks(); + chunk.before = util.fixEolChars(stateObj.text.substring(0, stateObj.start)); + chunk.startTag = ""; + chunk.selection = util.fixEolChars(stateObj.text.substring(stateObj.start, stateObj.end)); + chunk.endTag = ""; + chunk.after = util.fixEolChars(stateObj.text.substring(stateObj.end)); + chunk.scrollTop = stateObj.scrollTop; + + return chunk; + }; + + // Sets the TextareaState properties given a chunk of markdown. + this.setChunks = function (chunk) { + + chunk.before = chunk.before + chunk.startTag; + chunk.after = chunk.endTag + chunk.after; + + this.start = chunk.before.length; + this.end = chunk.before.length + chunk.selection.length; + this.text = chunk.before + chunk.selection + chunk.after; + this.scrollTop = chunk.scrollTop; + }; + this.init(); + }; + + function PreviewManager(converter, panels, previewRefreshCallback) { + + var managerObj = this; + var timeout; + var elapsedTime; + var oldInputText; + var maxDelay = 3000; + var startType = "delayed"; // The other legal value is "manual" + + // Adds event listeners to elements + var setupEvents = function (inputElem, listener) { + + util.addEvent(inputElem, "input", listener); + inputElem.onpaste = listener; + inputElem.ondrop = listener; + + util.addEvent(inputElem, "keypress", listener); + util.addEvent(inputElem, "keydown", listener); + }; + + var getDocScrollTop = function () { + + var result = 0; + + if (window.innerHeight) { + result = window.pageYOffset; + } + else + if (doc.documentElement && doc.documentElement.scrollTop) { + result = doc.documentElement.scrollTop; + } + else + if (doc.body) { + result = doc.body.scrollTop; + } + + return result; + }; + + var makePreviewHtml = function () { + + // If there is no registered preview panel + // there is nothing to do. + if (!panels.preview) + return; + + + var text = panels.input.value; + if (text && text == oldInputText) { + return; // Input text hasn't changed. + } + else { + oldInputText = text; + } + + var prevTime = new Date().getTime(); + + text = converter.makeHtml(text); + + // Calculate the processing time of the HTML creation. + // It's used as the delay time in the event listener. + var currTime = new Date().getTime(); + elapsedTime = currTime - prevTime; + + pushPreviewHtml(text); + }; + + // setTimeout is already used. Used as an event listener. + var applyTimeout = function () { + + if (timeout) { + clearTimeout(timeout); + timeout = undefined; + } + + if (startType !== "manual") { + + var delay = 0; + + if (startType === "delayed") { + delay = elapsedTime; + } + + if (delay > maxDelay) { + delay = maxDelay; + } + timeout = setTimeout(makePreviewHtml, delay); + } + }; + + var getScaleFactor = function (panel) { + if (panel.scrollHeight <= panel.clientHeight) { + return 1; + } + return panel.scrollTop / (panel.scrollHeight - panel.clientHeight); + }; + + var setPanelScrollTops = function () { + if (panels.preview) { + panels.preview.scrollTop = (panels.preview.scrollHeight - panels.preview.clientHeight) * getScaleFactor(panels.preview); + } + }; + + this.refresh = function (requiresRefresh) { + + if (requiresRefresh) { + oldInputText = ""; + makePreviewHtml(); + } + else { + applyTimeout(); + } + }; + + this.processingTime = function () { + return elapsedTime; + }; + + var isFirstTimeFilled = true; + + // IE doesn't let you use innerHTML if the element is contained somewhere in a table + // (which is the case for inline editing) -- in that case, detach the element, set the + // value, and reattach. Yes, that *is* ridiculous. + var ieSafePreviewSet = function (text) { + var preview = panels.preview; + var parent = preview.parentNode; + var sibling = preview.nextSibling; + parent.removeChild(preview); + preview.innerHTML = text; + if (!sibling) + parent.appendChild(preview); + else + parent.insertBefore(preview, sibling); + } + + var nonSuckyBrowserPreviewSet = function (text) { + panels.preview.innerHTML = text; + } + + var previewSetter; + + var previewSet = function (text) { + if (previewSetter) + return previewSetter(text); + + try { + nonSuckyBrowserPreviewSet(text); + previewSetter = nonSuckyBrowserPreviewSet; + } catch (e) { + previewSetter = ieSafePreviewSet; + previewSetter(text); + } + }; + + var pushPreviewHtml = function (text) { + + var emptyTop = position.getTop(panels.input) - getDocScrollTop(); + + if (panels.preview) { + previewSet(text); + previewRefreshCallback(); + } + + setPanelScrollTops(); + + if (isFirstTimeFilled) { + isFirstTimeFilled = false; + return; + } + + var fullTop = position.getTop(panels.input) - getDocScrollTop(); + + if (uaSniffed.isIE) { + setTimeout(function () { + window.scrollBy(0, fullTop - emptyTop); + }, 0); + } + else { + window.scrollBy(0, fullTop - emptyTop); + } + }; + + var init = function () { + + setupEvents(panels.input, applyTimeout); + makePreviewHtml(); + + if (panels.preview) { + panels.preview.scrollTop = 0; + } + }; + + init(); + }; + + // Creates the background behind the hyperlink text entry box. + // And download dialog + // Most of this has been moved to CSS but the div creation and + // browser-specific hacks remain here. + ui.createBackground = function () { + + var background = doc.createElement("div"), + style = background.style; + + background.className = "wmd-prompt-background"; + + style.position = "absolute"; + style.top = "0"; + + style.zIndex = "1000"; + + if (uaSniffed.isIE) { + style.filter = "alpha(opacity=50)"; + } + else { + style.opacity = "0.5"; + } + + var pageSize = position.getPageSize(); + style.height = pageSize[1] + "px"; + + if (uaSniffed.isIE) { + style.left = doc.documentElement.scrollLeft; + style.width = doc.documentElement.clientWidth; + } + else { + style.left = "0"; + style.width = "100%"; + } + + doc.body.appendChild(background); + return background; + }; + + // This simulates a modal dialog box and asks for the URL when you + // click the hyperlink or image buttons. + // + // text: The html for the input box. + // defaultInputText: The default value that appears in the input box. + // callback: The function which is executed when the prompt is dismissed, either via OK or Cancel. + // It receives a single argument; either the entered text (if OK was chosen) or null (if Cancel + // was chosen). + ui.prompt = function (text, defaultInputText, callback) { + + // These variables need to be declared at this level since they are used + // in multiple functions. + var dialog; // The dialog box. + var input; // The text box where you enter the hyperlink. + + + if (defaultInputText === undefined) { + defaultInputText = ""; + } + + // Used as a keydown event handler. Esc dismisses the prompt. + // Key code 27 is ESC. + var checkEscape = function (key) { + var code = (key.charCode || key.keyCode); + if (code === 27) { + if (key.stopPropagation) key.stopPropagation(); + close(true); + return false; + } + }; + + // Dismisses the hyperlink input box. + // isCancel is true if we don't care about the input text. + // isCancel is false if we are going to keep the text. + var close = function (isCancel) { + util.removeEvent(doc.body, "keyup", checkEscape); + var text = input.value; + + if (isCancel) { + text = null; + } + else { + // Fixes common pasting errors. + text = text.replace(/^http:\/\/(https?|ftp):\/\//, '$1://'); + if (!/^(?:https?|ftp):\/\//.test(text)) + text = 'http://' + text; + } + + dialog.parentNode.removeChild(dialog); + + callback(text); + return false; + }; + + + + // Create the text input box form/window. + var createDialog = function () { + + // The main dialog box. + dialog = doc.createElement("div"); + dialog.className = "wmd-prompt-dialog"; + dialog.style.padding = "10px;"; + dialog.style.position = "fixed"; + dialog.style.width = "400px"; + dialog.style.zIndex = "1001"; + + // The dialog text. + var question = doc.createElement("div"); + question.innerHTML = text; + question.style.padding = "5px"; + dialog.appendChild(question); + + // The web form container for the text box and buttons. + var form = doc.createElement("form"), + style = form.style; + form.onsubmit = function () { return close(false); }; + style.padding = "0"; + style.margin = "0"; + style.cssFloat = "left"; + style.width = "100%"; + style.textAlign = "center"; + style.position = "relative"; + dialog.appendChild(form); + + // The input text box + input = doc.createElement("input"); + input.type = "text"; + input.value = defaultInputText; + style = input.style; + style.display = "block"; + style.width = "80%"; + style.marginLeft = style.marginRight = "auto"; + form.appendChild(input); + + // The ok button + var okButton = doc.createElement("input"); + okButton.type = "button"; + okButton.onclick = function () { return close(false); }; + okButton.value = "OK"; + style = okButton.style; + style.margin = "10px"; + style.display = "inline"; + style.width = "7em"; + + + // The cancel button + var cancelButton = doc.createElement("input"); + cancelButton.type = "button"; + cancelButton.onclick = function () { return close(true); }; + cancelButton.value = "Cancel"; + style = cancelButton.style; + style.margin = "10px"; + style.display = "inline"; + style.width = "7em"; + + form.appendChild(okButton); + form.appendChild(cancelButton); + + util.addEvent(doc.body, "keyup", checkEscape); + dialog.style.top = "50%"; + dialog.style.left = "50%"; + dialog.style.display = "block"; + if (uaSniffed.isIE_5or6) { + dialog.style.position = "absolute"; + dialog.style.top = doc.documentElement.scrollTop + 200 + "px"; + dialog.style.left = "50%"; + } + doc.body.appendChild(dialog); + + // This has to be done AFTER adding the dialog to the form if you + // want it to be centered. + dialog.style.marginTop = -(position.getHeight(dialog) / 2) + "px"; + dialog.style.marginLeft = -(position.getWidth(dialog) / 2) + "px"; + + }; + + // Why is this in a zero-length timeout? + // Is it working around a browser bug? + setTimeout(function () { + + createDialog(); + + var defTextLen = defaultInputText.length; + if (input.selectionStart !== undefined) { + input.selectionStart = 0; + input.selectionEnd = defTextLen; + } + else if (input.createTextRange) { + var range = input.createTextRange(); + range.collapse(false); + range.moveStart("character", -defTextLen); + range.moveEnd("character", defTextLen); + range.select(); + } + + input.focus(); + }, 0); + }; + + function UIManager(postfix, panels, undoManager, previewManager, commandManager, helpOptions, getString) { + + var inputBox = panels.input, + buttons = {}; // buttons.undo, buttons.link, etc. The actual DOM elements. + + makeSpritedButtonRow(); + + var keyEvent = "keydown"; + if (uaSniffed.isOpera) { + keyEvent = "keypress"; + } + + util.addEvent(inputBox, keyEvent, function (key) { + + // Check to see if we have a button key and, if so execute the callback. + if ((key.ctrlKey || key.metaKey) && !key.altKey && !key.shiftKey) { + + var keyCode = key.charCode || key.keyCode; + var keyCodeStr = String.fromCharCode(keyCode).toLowerCase(); + + switch (keyCodeStr) { + case "b": + doClick(buttons.bold); + break; + case "i": + doClick(buttons.italic); + break; + case "l": + doClick(buttons.link); + break; + case "q": + doClick(buttons.quote); + break; + case "k": + doClick(buttons.code); + break; + case "g": + doClick(buttons.image); + break; + case "o": + doClick(buttons.olist); + break; + case "u": + doClick(buttons.ulist); + break; + case "h": + doClick(buttons.heading); + break; + case "r": + doClick(buttons.hr); + break; + case "y": + doClick(buttons.redo); + break; + case "z": + if (key.shiftKey) { + doClick(buttons.redo); + } + else { + doClick(buttons.undo); + } + break; + default: + return; + } + + + if (key.preventDefault) { + key.preventDefault(); + } + + if (window.event) { + window.event.returnValue = false; + } + } + }); + + // Auto-indent on shift-enter + util.addEvent(inputBox, "keyup", function (key) { + if (key.shiftKey && !key.ctrlKey && !key.metaKey) { + var keyCode = key.charCode || key.keyCode; + // Character 13 is Enter + if (keyCode === 13) { + var fakeButton = {}; + fakeButton.textOp = bindCommand("doAutoindent"); + doClick(fakeButton); + } + } + }); + + // special handler because IE clears the context of the textbox on ESC + if (uaSniffed.isIE) { + util.addEvent(inputBox, "keydown", function (key) { + var code = key.keyCode; + if (code === 27) { + return false; + } + }); + } + + + // Perform the button's action. + function doClick(button) { + + inputBox.focus(); + + if (button.textOp) { + + if (undoManager) { + undoManager.setCommandMode(); + } + + var state = new TextareaState(panels); + + if (!state) { + return; + } + + var chunks = state.getChunks(); + + // Some commands launch a "modal" prompt dialog. Javascript + // can't really make a modal dialog box and the WMD code + // will continue to execute while the dialog is displayed. + // This prevents the dialog pattern I'm used to and means + // I can't do something like this: + // + // var link = CreateLinkDialog(); + // makeMarkdownLink(link); + // + // Instead of this straightforward method of handling a + // dialog I have to pass any code which would execute + // after the dialog is dismissed (e.g. link creation) + // in a function parameter. + // + // Yes this is awkward and I think it sucks, but there's + // no real workaround. Only the image and link code + // create dialogs and require the function pointers. + var fixupInputArea = function () { + + inputBox.focus(); + + if (chunks) { + state.setChunks(chunks); + } + + state.restore(); + previewManager.refresh(); + }; + + var noCleanup = button.textOp(chunks, fixupInputArea); + + if (!noCleanup) { + fixupInputArea(); + } + + } + + if (button.execute) { + button.execute(undoManager); + } + }; + + function setupButton(button, isEnabled) { + + var normalYShift = "0px"; + var disabledYShift = "-20px"; + var highlightYShift = "-40px"; + var image = button.getElementsByTagName("span")[0]; + if (isEnabled) { + image.style.backgroundPosition = button.XShift + " " + normalYShift; + button.onmouseover = function () { + image.style.backgroundPosition = this.XShift + " " + highlightYShift; + }; + + button.onmouseout = function () { + image.style.backgroundPosition = this.XShift + " " + normalYShift; + }; + + // IE tries to select the background image "button" text (it's + // implemented in a list item) so we have to cache the selection + // on mousedown. + if (uaSniffed.isIE) { + button.onmousedown = function () { + if (doc.activeElement && doc.activeElement !== panels.input) { // we're not even in the input box, so there's no selection + return; + } + panels.ieCachedRange = document.selection.createRange(); + panels.ieCachedScrollTop = panels.input.scrollTop; + }; + } + + if (!button.isHelp) { + button.onclick = function () { + if (this.onmouseout) { + this.onmouseout(); + } + doClick(this); + return false; + } + } + } + else { + image.style.backgroundPosition = button.XShift + " " + disabledYShift; + button.onmouseover = button.onmouseout = button.onclick = function () { }; + } + } + + function bindCommand(method) { + if (typeof method === "string") + method = commandManager[method]; + return function () { method.apply(commandManager, arguments); } + } + + function makeSpritedButtonRow() { + + var buttonBar = panels.buttonBar; + + var normalYShift = "0px"; + var disabledYShift = "-20px"; + var highlightYShift = "-40px"; + + var buttonRow = document.createElement("ul"); + buttonRow.id = "wmd-button-row" + postfix; + buttonRow.className = 'wmd-button-row'; + buttonRow = buttonBar.appendChild(buttonRow); + var xPosition = 0; + var makeButton = function (id, title, XShift, textOp) { + var button = document.createElement("li"); + button.className = "wmd-button"; + button.style.left = xPosition + "px"; + xPosition += 25; + var buttonImage = document.createElement("span"); + button.id = id + postfix; + button.appendChild(buttonImage); + button.title = title; + button.XShift = XShift; + if (textOp) + button.textOp = textOp; + setupButton(button, true); + buttonRow.appendChild(button); + return button; + }; + var makeSpacer = function (num) { + var spacer = document.createElement("li"); + spacer.className = "wmd-spacer wmd-spacer" + num; + spacer.id = "wmd-spacer" + num + postfix; + buttonRow.appendChild(spacer); + xPosition += 25; + } + + buttons.bold = makeButton("wmd-bold-button", getString("bold"), "0px", bindCommand("doBold")); + buttons.italic = makeButton("wmd-italic-button", getString("italic"), "-20px", bindCommand("doItalic")); + makeSpacer(1); + buttons.link = makeButton("wmd-link-button", getString("link"), "-40px", bindCommand(function (chunk, postProcessing) { + return this.doLinkOrImage(chunk, postProcessing, false); + })); + buttons.quote = makeButton("wmd-quote-button", getString("quote"), "-60px", bindCommand("doBlockquote")); + buttons.code = makeButton("wmd-code-button", getString("code"), "-80px", bindCommand("doCode")); + buttons.image = makeButton("wmd-image-button", getString("image"), "-100px", bindCommand(function (chunk, postProcessing) { + return this.doLinkOrImage(chunk, postProcessing, true); + })); + makeSpacer(2); + buttons.olist = makeButton("wmd-olist-button", getString("olist"), "-120px", bindCommand(function (chunk, postProcessing) { + this.doList(chunk, postProcessing, true); + })); + buttons.ulist = makeButton("wmd-ulist-button", getString("ulist"), "-140px", bindCommand(function (chunk, postProcessing) { + this.doList(chunk, postProcessing, false); + })); + buttons.heading = makeButton("wmd-heading-button", getString("heading"), "-160px", bindCommand("doHeading")); + buttons.hr = makeButton("wmd-hr-button", getString("hr"), "-180px", bindCommand("doHorizontalRule")); + makeSpacer(3); + buttons.undo = makeButton("wmd-undo-button", getString("undo"), "-200px", null); + buttons.undo.execute = function (manager) { if (manager) manager.undo(); }; + + var redoTitle = /win/.test(nav.platform.toLowerCase()) ? + getString("redo") : + getString("redomac"); // mac and other non-Windows platforms + + buttons.redo = makeButton("wmd-redo-button", redoTitle, "-220px", null); + buttons.redo.execute = function (manager) { if (manager) manager.redo(); }; + + if (helpOptions) { + var helpButton = document.createElement("li"); + var helpButtonImage = document.createElement("span"); + helpButton.appendChild(helpButtonImage); + helpButton.className = "wmd-button wmd-help-button"; + helpButton.id = "wmd-help-button" + postfix; + helpButton.XShift = "-240px"; + helpButton.isHelp = true; + helpButton.style.right = "0px"; + helpButton.title = getString("help"); + helpButton.onclick = helpOptions.handler; + + setupButton(helpButton, true); + buttonRow.appendChild(helpButton); + buttons.help = helpButton; + } + + setUndoRedoButtonStates(); + } + + function setUndoRedoButtonStates() { + if (undoManager) { + setupButton(buttons.undo, undoManager.canUndo()); + setupButton(buttons.redo, undoManager.canRedo()); + } + }; + + this.setUndoRedoButtonStates = setUndoRedoButtonStates; + + } + + function CommandManager(pluginHooks, getString, converter) { + this.hooks = pluginHooks; + this.getString = getString; + this.converter = converter; + } + + var commandProto = CommandManager.prototype; + + // The markdown symbols - 4 spaces = code, > = blockquote, etc. + commandProto.prefixes = "(?:\\s{4,}|\\s*>|\\s*-\\s+|\\s*\\d+\\.|=|\\+|-|_|\\*|#|\\s*\\[[^\n]]+\\]:)"; + + // Remove markdown symbols from the chunk selection. + commandProto.unwrap = function (chunk) { + var txt = new re("([^\\n])\\n(?!(\\n|" + this.prefixes + "))", "g"); + chunk.selection = chunk.selection.replace(txt, "$1 $2"); + }; + + commandProto.wrap = function (chunk, len) { + this.unwrap(chunk); + var regex = new re("(.{1," + len + "})( +|$\\n?)", "gm"), + that = this; + + chunk.selection = chunk.selection.replace(regex, function (line, marked) { + if (new re("^" + that.prefixes, "").test(line)) { + return line; + } + return marked + "\n"; + }); + + chunk.selection = chunk.selection.replace(/\s+$/, ""); + }; + + commandProto.doBold = function (chunk, postProcessing) { + return this.doBorI(chunk, postProcessing, 2, this.getString("boldexample")); + }; + + commandProto.doItalic = function (chunk, postProcessing) { + return this.doBorI(chunk, postProcessing, 1, this.getString("italicexample")); + }; + + // chunk: The selected region that will be enclosed with */** + // nStars: 1 for italics, 2 for bold + // insertText: If you just click the button without highlighting text, this gets inserted + commandProto.doBorI = function (chunk, postProcessing, nStars, insertText) { + + // Get rid of whitespace and fixup newlines. + chunk.trimWhitespace(); + chunk.selection = chunk.selection.replace(/\n{2,}/g, "\n"); + + // Look for stars before and after. Is the chunk already marked up? + // note that these regex matches cannot fail + var starsBefore = /(\**$)/.exec(chunk.before)[0]; + var starsAfter = /(^\**)/.exec(chunk.after)[0]; + + var prevStars = Math.min(starsBefore.length, starsAfter.length); + + // Remove stars if we have to since the button acts as a toggle. + if ((prevStars >= nStars) && (prevStars != 2 || nStars != 1)) { + chunk.before = chunk.before.replace(re("[*]{" + nStars + "}$", ""), ""); + chunk.after = chunk.after.replace(re("^[*]{" + nStars + "}", ""), ""); + } + else if (!chunk.selection && starsAfter) { + // It's not really clear why this code is necessary. It just moves + // some arbitrary stuff around. + chunk.after = chunk.after.replace(/^([*_]*)/, ""); + chunk.before = chunk.before.replace(/(\s?)$/, ""); + var whitespace = re.$1; + chunk.before = chunk.before + starsAfter + whitespace; + } + else { + + // In most cases, if you don't have any selected text and click the button + // you'll get a selected, marked up region with the default text inserted. + if (!chunk.selection && !starsAfter) { + chunk.selection = insertText; + } + + // Add the true markup. + var markup = nStars <= 1 ? "*" : "**"; // shouldn't the test be = ? + chunk.before = chunk.before + markup; + chunk.after = markup + chunk.after; + } + + return; + }; + + commandProto.stripLinkDefs = function (text, defsToAdd) { + + text = text.replace(/^[ ]{0,3}\[(\d+)\]:[ \t]*\n?[ \t]*?[ \t]*\n?[ \t]*(?:(\n*)["(](.+?)[")][ \t]*)?(?:\n+|$)/gm, + function (totalMatch, id, link, newlines, title) { + defsToAdd[id] = totalMatch.replace(/\s*$/, ""); + if (newlines) { + // Strip the title and return that separately. + defsToAdd[id] = totalMatch.replace(/["(](.+?)[")]$/, ""); + return newlines + title; + } + return ""; + }); + + return text; + }; + + commandProto.addLinkDef = function (chunk, linkDef) { + + var refNumber = 0; // The current reference number + var defsToAdd = {}; // + // Start with a clean slate by removing all previous link definitions. + chunk.before = this.stripLinkDefs(chunk.before, defsToAdd); + chunk.selection = this.stripLinkDefs(chunk.selection, defsToAdd); + chunk.after = this.stripLinkDefs(chunk.after, defsToAdd); + + var defs = ""; + var regex = /(\[)((?:\[[^\]]*\]|[^\[\]])*)(\][ ]?(?:\n[ ]*)?\[)(\d+)(\])/g; + + // The above regex, used to update [foo][13] references after renumbering, + // is much too liberal; it can catch things that are not actually parsed + // as references (notably: code). It's impossible to know which matches are + // real references without performing a markdown conversion, so that's what + // we do. All matches are replaced with a unique reference number, which is + // given a unique link. The uniquifier in both cases is the character offset + // of the match inside the source string. The modified version is then sent + // through the Markdown renderer. Because link reference are stripped during + // rendering, the unique link is present in the rendered version if and only + // if the match at its offset was in fact rendered as a link or image. + var complete = chunk.before + chunk.selection + chunk.after; + var rendered = this.converter.makeHtml(complete); + var testlink = "http://this-is-a-real-link.biz/"; + + // If our fake link appears in the rendered version *before* we have added it, + // this probably means you're a Meta Stack Exchange user who is deliberately + // trying to break this feature. You can still break this workaround if you + // attach a plugin to the converter that sometimes (!) inserts this link. In + // that case, consider yourself unsupported. + while (rendered.indexOf(testlink) != -1) + testlink += "nicetry/"; + + var fakedefs = "\n\n"; + + // the regex is tested on the (up to) three chunks separately, and on substrings, + // so in order to have the correct offsets to check against okayToModify(), we + // have to keep track of how many characters are in the original source before + // the substring that we're looking at. Note that doLinkOrImage aligns the selection + // on potential brackets, so there should be no major breakage from the chunk + // separation. + var skippedChars = 0; + + var uniquified = complete.replace(regex, function uniquify(wholeMatch, before, inner, afterInner, id, end, offset) { + skippedChars += offset; + fakedefs += " [" + skippedChars + "]: " + testlink + skippedChars + "/unicorn\n"; + skippedChars += before.length; + inner = inner.replace(regex, uniquify); + skippedChars -= before.length; + var result = before + inner + afterInner + skippedChars + end; + skippedChars -= offset; + return result; + }); + + rendered = this.converter.makeHtml(uniquified + fakedefs); + + var okayToModify = function(offset) { + return rendered.indexOf(testlink + offset + "/unicorn") !== -1; + } + + var addDefNumber = function (def) { + refNumber++; + def = def.replace(/^[ ]{0,3}\[(\d+)\]:/, " [" + refNumber + "]:"); + defs += "\n" + def; + }; + + // note that + // a) the recursive call to getLink cannot go infinite, because by definition + // of regex, inner is always a proper substring of wholeMatch, and + // b) more than one level of nesting is neither supported by the regex + // nor making a lot of sense (the only use case for nesting is a linked image) + var getLink = function (wholeMatch, before, inner, afterInner, id, end, offset) { + if (!okayToModify(skippedChars + offset)) + return wholeMatch; + skippedChars += offset + before.length; + inner = inner.replace(regex, getLink); + skippedChars -= offset + before.length; + if (defsToAdd[id]) { + addDefNumber(defsToAdd[id]); + return before + inner + afterInner + refNumber + end; + } + return wholeMatch; + }; + + var len = chunk.before.length; + chunk.before = chunk.before.replace(regex, getLink); + skippedChars += len; + + len = chunk.selection.length; + if (linkDef) { + addDefNumber(linkDef); + } + else { + chunk.selection = chunk.selection.replace(regex, getLink); + } + skippedChars += len; + + var refOut = refNumber; + + chunk.after = chunk.after.replace(regex, getLink); + + if (chunk.after) { + chunk.after = chunk.after.replace(/\n*$/, ""); + } + if (!chunk.after) { + chunk.selection = chunk.selection.replace(/\n*$/, ""); + } + + chunk.after += "\n\n" + defs; + + return refOut; + }; + + // takes the line as entered into the add link/as image dialog and makes + // sure the URL and the optinal title are "nice". + function properlyEncoded(linkdef) { + return linkdef.replace(/^\s*(.*?)(?:\s+"(.+)")?\s*$/, function (wholematch, link, title) { + + var inQueryString = false; + + // Having `[^\w\d-./]` in there is just a shortcut that lets us skip + // the most common characters in URLs. Replacing that it with `.` would not change + // the result, because encodeURI returns those characters unchanged, but it + // would mean lots of unnecessary replacement calls. Having `[` and `]` in that + // section as well means we do *not* enocde square brackets. These characters are + // a strange beast in URLs, but if anything, this causes URLs to be more readable, + // and we leave it to the browser to make sure that these links are handled without + // problems. + link = link.replace(/%(?:[\da-fA-F]{2})|\?|\+|[^\w\d-./[\]]/g, function (match) { + // Valid percent encoding. Could just return it as is, but we follow RFC3986 + // Section 2.1 which says "For consistency, URI producers and normalizers + // should use uppercase hexadecimal digits for all percent-encodings." + // Note that we also handle (illegal) stand-alone percent characters by + // replacing them with "%25" + if (match.length === 3 && match.charAt(0) == "%") { + return match.toUpperCase(); + } + switch (match) { + case "?": + inQueryString = true; + return "?"; + break; + + // In the query string, a plus and a space are identical -- normalize. + // Not strictly necessary, but identical behavior to the previous version + // of this function. + case "+": + if (inQueryString) + return "%20"; + break; + } + return encodeURI(match); + }) + + if (title) { + title = title.trim ? title.trim() : title.replace(/^\s*/, "").replace(/\s*$/, ""); + title = title.replace(/"/g, "quot;").replace(/\(/g, "(").replace(/\)/g, ")").replace(//g, ">"); + } + return title ? link + ' "' + title + '"' : link; + }); + } + + commandProto.doLinkOrImage = function (chunk, postProcessing, isImage) { + + chunk.trimWhitespace(); + chunk.findTags(/\s*!?\[/, /\][ ]?(?:\n[ ]*)?(\[.*?\])?/); + var background; + + if (chunk.endTag.length > 1 && chunk.startTag.length > 0) { + + chunk.startTag = chunk.startTag.replace(/!?\[/, ""); + chunk.endTag = ""; + this.addLinkDef(chunk, null); + + } + else { + + // We're moving start and end tag back into the selection, since (as we're in the else block) we're not + // *removing* a link, but *adding* one, so whatever findTags() found is now back to being part of the + // link text. linkEnteredCallback takes care of escaping any brackets. + chunk.selection = chunk.startTag + chunk.selection + chunk.endTag; + chunk.startTag = chunk.endTag = ""; + + if (/\n\n/.test(chunk.selection)) { + this.addLinkDef(chunk, null); + return; + } + var that = this; + // The function to be executed when you enter a link and press OK or Cancel. + // Marks up the link and adds the ref. + var linkEnteredCallback = function (link) { + + background.parentNode.removeChild(background); + + if (link !== null) { + // ( $1 + // [^\\] anything that's not a backslash + // (?:\\\\)* an even number (this includes zero) of backslashes + // ) + // (?= followed by + // [[\]] an opening or closing bracket + // ) + // + // In other words, a non-escaped bracket. These have to be escaped now to make sure they + // don't count as the end of the link or similar. + // Note that the actual bracket has to be a lookahead, because (in case of to subsequent brackets), + // the bracket in one match may be the "not a backslash" character in the next match, so it + // should not be consumed by the first match. + // The "prepend a space and finally remove it" steps makes sure there is a "not a backslash" at the + // start of the string, so this also works if the selection begins with a bracket. We cannot solve + // this by anchoring with ^, because in the case that the selection starts with two brackets, this + // would mean a zero-width match at the start. Since zero-width matches advance the string position, + // the first bracket could then not act as the "not a backslash" for the second. + chunk.selection = (" " + chunk.selection).replace(/([^\\](?:\\\\)*)(?=[[\]])/g, "$1\\").substr(1); + + var linkDef = " [999]: " + properlyEncoded(link); + + var num = that.addLinkDef(chunk, linkDef); + chunk.startTag = isImage ? "![" : "["; + chunk.endTag = "][" + num + "]"; + + if (!chunk.selection) { + if (isImage) { + chunk.selection = that.getString("imagedescription"); + } + else { + chunk.selection = that.getString("linkdescription"); + } + } + } + postProcessing(); + }; + + background = ui.createBackground(); + + if (isImage) { + if (!this.hooks.insertImageDialog(linkEnteredCallback)) + ui.prompt(this.getString("imagedialog"), imageDefaultText, linkEnteredCallback); + } + else { + ui.prompt(this.getString("linkdialog"), linkDefaultText, linkEnteredCallback); + } + return true; + } + }; + + // When making a list, hitting shift-enter will put your cursor on the next line + // at the current indent level. + commandProto.doAutoindent = function (chunk, postProcessing) { + + var commandMgr = this, + fakeSelection = false; + + chunk.before = chunk.before.replace(/(\n|^)[ ]{0,3}([*+-]|\d+[.])[ \t]*\n$/, "\n\n"); + chunk.before = chunk.before.replace(/(\n|^)[ ]{0,3}>[ \t]*\n$/, "\n\n"); + chunk.before = chunk.before.replace(/(\n|^)[ \t]+\n$/, "\n\n"); + + // There's no selection, end the cursor wasn't at the end of the line: + // The user wants to split the current list item / code line / blockquote line + // (for the latter it doesn't really matter) in two. Temporarily select the + // (rest of the) line to achieve this. + if (!chunk.selection && !/^[ \t]*(?:\n|$)/.test(chunk.after)) { + chunk.after = chunk.after.replace(/^[^\n]*/, function (wholeMatch) { + chunk.selection = wholeMatch; + return ""; + }); + fakeSelection = true; + } + + if (/(\n|^)[ ]{0,3}([*+-]|\d+[.])[ \t]+.*\n$/.test(chunk.before)) { + if (commandMgr.doList) { + commandMgr.doList(chunk); + } + } + if (/(\n|^)[ ]{0,3}>[ \t]+.*\n$/.test(chunk.before)) { + if (commandMgr.doBlockquote) { + commandMgr.doBlockquote(chunk); + } + } + if (/(\n|^)(\t|[ ]{4,}).*\n$/.test(chunk.before)) { + if (commandMgr.doCode) { + commandMgr.doCode(chunk); + } + } + + if (fakeSelection) { + chunk.after = chunk.selection + chunk.after; + chunk.selection = ""; + } + }; + + commandProto.doBlockquote = function (chunk, postProcessing) { + + chunk.selection = chunk.selection.replace(/^(\n*)([^\r]+?)(\n*)$/, + function (totalMatch, newlinesBefore, text, newlinesAfter) { + chunk.before += newlinesBefore; + chunk.after = newlinesAfter + chunk.after; + return text; + }); + + chunk.before = chunk.before.replace(/(>[ \t]*)$/, + function (totalMatch, blankLine) { + chunk.selection = blankLine + chunk.selection; + return ""; + }); + + chunk.selection = chunk.selection.replace(/^(\s|>)+$/, ""); + chunk.selection = chunk.selection || this.getString("quoteexample"); + + // The original code uses a regular expression to find out how much of the + // text *directly before* the selection already was a blockquote: + + /* + if (chunk.before) { + chunk.before = chunk.before.replace(/\n?$/, "\n"); + } + chunk.before = chunk.before.replace(/(((\n|^)(\n[ \t]*)*>(.+\n)*.*)+(\n[ \t]*)*$)/, + function (totalMatch) { + chunk.startTag = totalMatch; + return ""; + }); + */ + + // This comes down to: + // Go backwards as many lines a possible, such that each line + // a) starts with ">", or + // b) is almost empty, except for whitespace, or + // c) is preceeded by an unbroken chain of non-empty lines + // leading up to a line that starts with ">" and at least one more character + // and in addition + // d) at least one line fulfills a) + // + // Since this is essentially a backwards-moving regex, it's susceptible to + // catstrophic backtracking and can cause the browser to hang; + // see e.g. http://meta.stackexchange.com/questions/9807. + // + // Hence we replaced this by a simple state machine that just goes through the + // lines and checks for a), b), and c). + + var match = "", + leftOver = "", + line; + if (chunk.before) { + var lines = chunk.before.replace(/\n$/, "").split("\n"); + var inChain = false; + for (var i = 0; i < lines.length; i++) { + var good = false; + line = lines[i]; + inChain = inChain && line.length > 0; // c) any non-empty line continues the chain + if (/^>/.test(line)) { // a) + good = true; + if (!inChain && line.length > 1) // c) any line that starts with ">" and has at least one more character starts the chain + inChain = true; + } else if (/^[ \t]*$/.test(line)) { // b) + good = true; + } else { + good = inChain; // c) the line is not empty and does not start with ">", so it matches if and only if we're in the chain + } + if (good) { + match += line + "\n"; + } else { + leftOver += match + line; + match = "\n"; + } + } + if (!/(^|\n)>/.test(match)) { // d) + leftOver += match; + match = ""; + } + } + + chunk.startTag = match; + chunk.before = leftOver; + + // end of change + + if (chunk.after) { + chunk.after = chunk.after.replace(/^\n?/, "\n"); + } + + chunk.after = chunk.after.replace(/^(((\n|^)(\n[ \t]*)*>(.+\n)*.*)+(\n[ \t]*)*)/, + function (totalMatch) { + chunk.endTag = totalMatch; + return ""; + } + ); + + var replaceBlanksInTags = function (useBracket) { + + var replacement = useBracket ? "> " : ""; + + if (chunk.startTag) { + chunk.startTag = chunk.startTag.replace(/\n((>|\s)*)\n$/, + function (totalMatch, markdown) { + return "\n" + markdown.replace(/^[ ]{0,3}>?[ \t]*$/gm, replacement) + "\n"; + }); + } + if (chunk.endTag) { + chunk.endTag = chunk.endTag.replace(/^\n((>|\s)*)\n/, + function (totalMatch, markdown) { + return "\n" + markdown.replace(/^[ ]{0,3}>?[ \t]*$/gm, replacement) + "\n"; + }); + } + }; + + if (/^(?![ ]{0,3}>)/m.test(chunk.selection)) { + this.wrap(chunk, SETTINGS.lineLength - 2); + chunk.selection = chunk.selection.replace(/^/gm, "> "); + replaceBlanksInTags(true); + chunk.skipLines(); + } else { + chunk.selection = chunk.selection.replace(/^[ ]{0,3}> ?/gm, ""); + this.unwrap(chunk); + replaceBlanksInTags(false); + + if (!/^(\n|^)[ ]{0,3}>/.test(chunk.selection) && chunk.startTag) { + chunk.startTag = chunk.startTag.replace(/\n{0,2}$/, "\n\n"); + } + + if (!/(\n|^)[ ]{0,3}>.*$/.test(chunk.selection) && chunk.endTag) { + chunk.endTag = chunk.endTag.replace(/^\n{0,2}/, "\n\n"); + } + } + + chunk.selection = this.hooks.postBlockquoteCreation(chunk.selection); + + if (!/\n/.test(chunk.selection)) { + chunk.selection = chunk.selection.replace(/^(> *)/, + function (wholeMatch, blanks) { + chunk.startTag += blanks; + return ""; + }); + } + }; + + commandProto.doCode = function (chunk, postProcessing) { + + var hasTextBefore = /\S[ ]*$/.test(chunk.before); + var hasTextAfter = /^[ ]*\S/.test(chunk.after); + + // Use 'four space' markdown if the selection is on its own + // line or is multiline. + if ((!hasTextAfter && !hasTextBefore) || /\n/.test(chunk.selection)) { + + chunk.before = chunk.before.replace(/[ ]{4}$/, + function (totalMatch) { + chunk.selection = totalMatch + chunk.selection; + return ""; + }); + + var nLinesBack = 1; + var nLinesForward = 1; + + if (/(\n|^)(\t|[ ]{4,}).*\n$/.test(chunk.before)) { + nLinesBack = 0; + } + if (/^\n(\t|[ ]{4,})/.test(chunk.after)) { + nLinesForward = 0; + } + + chunk.skipLines(nLinesBack, nLinesForward); + + if (!chunk.selection) { + chunk.startTag = " "; + chunk.selection = this.getString("codeexample"); + } + else { + if (/^[ ]{0,3}\S/m.test(chunk.selection)) { + if (/\n/.test(chunk.selection)) + chunk.selection = chunk.selection.replace(/^/gm, " "); + else // if it's not multiline, do not select the four added spaces; this is more consistent with the doList behavior + chunk.before += " "; + } + else { + chunk.selection = chunk.selection.replace(/^(?:[ ]{4}|[ ]{0,3}\t)/gm, ""); + } + } + } + else { + // Use backticks (`) to delimit the code block. + + chunk.trimWhitespace(); + chunk.findTags(/`/, /`/); + + if (!chunk.startTag && !chunk.endTag) { + chunk.startTag = chunk.endTag = "`"; + if (!chunk.selection) { + chunk.selection = this.getString("codeexample"); + } + } + else if (chunk.endTag && !chunk.startTag) { + chunk.before += chunk.endTag; + chunk.endTag = ""; + } + else { + chunk.startTag = chunk.endTag = ""; + } + } + }; + + commandProto.doList = function (chunk, postProcessing, isNumberedList) { + + // These are identical except at the very beginning and end. + // Should probably use the regex extension function to make this clearer. + var previousItemsRegex = /(\n|^)(([ ]{0,3}([*+-]|\d+[.])[ \t]+.*)(\n.+|\n{2,}([*+-].*|\d+[.])[ \t]+.*|\n{2,}[ \t]+\S.*)*)\n*$/; + var nextItemsRegex = /^\n*(([ ]{0,3}([*+-]|\d+[.])[ \t]+.*)(\n.+|\n{2,}([*+-].*|\d+[.])[ \t]+.*|\n{2,}[ \t]+\S.*)*)\n*/; + + // The default bullet is a dash but others are possible. + // This has nothing to do with the particular HTML bullet, + // it's just a markdown bullet. + var bullet = "-"; + + // The number in a numbered list. + var num = 1; + + // Get the item prefix - e.g. " 1. " for a numbered list, " - " for a bulleted list. + var getItemPrefix = function () { + var prefix; + if (isNumberedList) { + prefix = " " + num + ". "; + num++; + } + else { + prefix = " " + bullet + " "; + } + return prefix; + }; + + // Fixes the prefixes of the other list items. + var getPrefixedItem = function (itemText) { + + // The numbering flag is unset when called by autoindent. + if (isNumberedList === undefined) { + isNumberedList = /^\s*\d/.test(itemText); + } + + // Renumber/bullet the list element. + itemText = itemText.replace(/^[ ]{0,3}([*+-]|\d+[.])\s/gm, + function (_) { + return getItemPrefix(); + }); + + return itemText; + }; + + chunk.findTags(/(\n|^)*[ ]{0,3}([*+-]|\d+[.])\s+/, null); + + if (chunk.before && !/\n$/.test(chunk.before) && !/^\n/.test(chunk.startTag)) { + chunk.before += chunk.startTag; + chunk.startTag = ""; + } + + if (chunk.startTag) { + + var hasDigits = /\d+[.]/.test(chunk.startTag); + chunk.startTag = ""; + chunk.selection = chunk.selection.replace(/\n[ ]{4}/g, "\n"); + this.unwrap(chunk); + chunk.skipLines(); + + if (hasDigits) { + // Have to renumber the bullet points if this is a numbered list. + chunk.after = chunk.after.replace(nextItemsRegex, getPrefixedItem); + } + if (isNumberedList == hasDigits) { + return; + } + } + + var nLinesUp = 1; + + chunk.before = chunk.before.replace(previousItemsRegex, + function (itemText) { + if (/^\s*([*+-])/.test(itemText)) { + bullet = re.$1; + } + nLinesUp = /[^\n]\n\n[^\n]/.test(itemText) ? 1 : 0; + return getPrefixedItem(itemText); + }); + + if (!chunk.selection) { + chunk.selection = this.getString("litem"); + } + + var prefix = getItemPrefix(); + + var nLinesDown = 1; + + chunk.after = chunk.after.replace(nextItemsRegex, + function (itemText) { + nLinesDown = /[^\n]\n\n[^\n]/.test(itemText) ? 1 : 0; + return getPrefixedItem(itemText); + }); + + chunk.trimWhitespace(true); + chunk.skipLines(nLinesUp, nLinesDown, true); + chunk.startTag = prefix; + var spaces = prefix.replace(/./g, " "); + this.wrap(chunk, SETTINGS.lineLength - spaces.length); + chunk.selection = chunk.selection.replace(/\n/g, "\n" + spaces); + + }; + + commandProto.doHeading = function (chunk, postProcessing) { + + // Remove leading/trailing whitespace and reduce internal spaces to single spaces. + chunk.selection = chunk.selection.replace(/\s+/g, " "); + chunk.selection = chunk.selection.replace(/(^\s+|\s+$)/g, ""); + + // If we clicked the button with no selected text, we just + // make a level 2 hash header around some default text. + if (!chunk.selection) { + chunk.startTag = "## "; + chunk.selection = this.getString("headingexample"); + chunk.endTag = " ##"; + return; + } + + var headerLevel = 0; // The existing header level of the selected text. + + // Remove any existing hash heading markdown and save the header level. + chunk.findTags(/#+[ ]*/, /[ ]*#+/); + if (/#+/.test(chunk.startTag)) { + headerLevel = re.lastMatch.length; + } + chunk.startTag = chunk.endTag = ""; + + // Try to get the current header level by looking for - and = in the line + // below the selection. + chunk.findTags(null, /\s?(-+|=+)/); + if (/=+/.test(chunk.endTag)) { + headerLevel = 1; + } + if (/-+/.test(chunk.endTag)) { + headerLevel = 2; + } + + // Skip to the next line so we can create the header markdown. + chunk.startTag = chunk.endTag = ""; + chunk.skipLines(1, 1); + + // We make a level 2 header if there is no current header. + // If there is a header level, we substract one from the header level. + // If it's already a level 1 header, it's removed. + var headerLevelToCreate = headerLevel == 0 ? 2 : headerLevel - 1; + + if (headerLevelToCreate > 0) { + + // The button only creates level 1 and 2 underline headers. + // Why not have it iterate over hash header levels? Wouldn't that be easier and cleaner? + var headerChar = headerLevelToCreate >= 2 ? "-" : "="; + var len = chunk.selection.length; + if (len > SETTINGS.lineLength) { + len = SETTINGS.lineLength; + } + chunk.endTag = "\n"; + while (len--) { + chunk.endTag += headerChar; + } + } + }; + + commandProto.doHorizontalRule = function (chunk, postProcessing) { + chunk.startTag = "----------\n"; + chunk.selection = ""; + chunk.skipLines(2, 1, true); + } + + +})(); \ No newline at end of file diff --git a/resources/js/pagedown/Markdown.Editor.modified.js b/resources/js/pagedown/Markdown.Editor.modified.js new file mode 100644 index 0000000000..f0dd0e1091 --- /dev/null +++ b/resources/js/pagedown/Markdown.Editor.modified.js @@ -0,0 +1,2444 @@ +// needs Markdown.Converter.js at the moment + + (function() { + + var util = {}, + position = {}, + ui = {}, + doc = window.document, + re = window.RegExp, + nav = window.navigator, + SETTINGS = {lineLength: 72}, + // Used to work around some browser bugs where we can't use feature testing. + uaSniffed = { + isIE: /msie/.test(nav.userAgent.toLowerCase()), + isIE_5or6: /msie 6/.test(nav.userAgent.toLowerCase()) || /msie 5/.test(nav.userAgent.toLowerCase()), + isOpera: /opera/.test(nav.userAgent.toLowerCase()) + }; + + var defaultsStrings = { + bold: "Strong Ctrl+B", + boldexample: "strong text", + italic: "Emphasis Ctrl+I", + italicexample: "emphasized text", + link: "Hyperlink Ctrl+L", + linkdescription: "enter link description here", + linkdialog: "

        Insert Hyperlink

        http://example.com/ \"optional title\"

        ", + quote: "Blockquote
        Ctrl+Q", + quoteexample: "Blockquote", + code: "Code Sample
         Ctrl+K",
        +                codeexample: "enter code here",
        +                image: "Image  Ctrl+G",
        +                imagedescription: "enter image description here",
        +                imagedialog: "

        Insert Image

        http://example.com/images/diagram.jpg \"optional title\"

        Need
        free image hosting?

        ", + olist: "Numbered List
          Ctrl+O", + ulist: "Bulleted List
            Ctrl+U", + litem: "List item", + heading: "Heading

            /

            Ctrl+H", + headingexample: "Heading", + hr: "Horizontal Rule
            Ctrl+R", + undo: "Undo - Ctrl+Z", + redo: "Redo - Ctrl+Y", + redomac: "Redo - Ctrl+Shift+Z", + help: "Markdown Editing Help" + }; + + + // ------------------------------------------------------------------- + // YOUR CHANGES GO HERE + // + // I've tried to localize the things you are likely to change to + // this area. + // ------------------------------------------------------------------- + + // The default text that appears in the dialog input box when entering + // links. + var imageDefaultText = "http://"; + var linkDefaultText = "http://"; + + // ------------------------------------------------------------------- + // END OF YOUR CHANGES + // ------------------------------------------------------------------- + + // options, if given, can have the following properties: + // options.helpButton = { handler: yourEventHandler } + // options.strings = { italicexample: "slanted text" } + // `yourEventHandler` is the click handler for the help button. + // If `options.helpButton` isn't given, not help button is created. + // `options.strings` can have any or all of the same properties as + // `defaultStrings` above, so you can just override some string displayed + // to the user on a case-by-case basis, or translate all strings to + // a different language. + // + // For backwards compatibility reasons, the `options` argument can also + // be just the `helpButton` object, and `strings.help` can also be set via + // `helpButton.title`. This should be considered legacy. + // + // The constructed editor object has the methods: + // - getConverter() returns the markdown converter object that was passed to the constructor + // - run() actually starts the editor; should be called after all necessary plugins are registered. Calling this more than once is a no-op. + // - refreshPreview() forces the preview to be updated. This method is only available after run() was called. + Markdown.Editor = function(markdownConverter, idPostfix, options) { + + options = options || {}; + + if(typeof options.handler === "function") { //backwards compatible behavior + options = {helpButton: options}; + } + options.strings = options.strings || {}; + if(options.helpButton) { + options.strings.help = options.strings.help || options.helpButton.title; + } + var getString = function(identifier) { + return options.strings[identifier] || defaultsStrings[identifier]; + } + + idPostfix = idPostfix || ""; + + var hooks = this.hooks = new Markdown.HookCollection(); + hooks.addNoop("onPreviewRefresh"); // called with no arguments after the preview has been refreshed + hooks.addNoop("postBlockquoteCreation"); // called with the user's selection *after* the blockquote was created; should return the actual to-be-inserted text + hooks.addFalse("insertImageDialog"); /* called with one parameter: a callback to be called with the URL of the image. If the application creates + * its own image insertion dialog, this hook should return true, and the callback should be called with the chosen + * image url (or null if the user cancelled). If this hook returns false, the default dialog will be used. + */ + + this.getConverter = function() { + return markdownConverter; + } + + var that = this, + panels; + + this.run = function() { + if(panels) + return; // already initialized + + panels = new PanelCollection(idPostfix); + var commandManager = new CommandManager(hooks, getString, markdownConverter); + var previewManager = new PreviewManager(markdownConverter, panels, function() { + hooks.onPreviewRefresh(); + }); + var undoManager, uiManager; + + if(!/\?noundo/.test(doc.location.href)) { + undoManager = new UndoManager(function() { + previewManager.refresh(); + if(uiManager) // not available on the first call + uiManager.setUndoRedoButtonStates(); + }, panels); + this.textOperation = function(f) { + undoManager.setCommandMode(); + f(); + that.refreshPreview(); + } + } + + uiManager = new UIManager(idPostfix, panels, undoManager, previewManager, commandManager, options.helpButton, getString); + uiManager.setUndoRedoButtonStates(); + + var forceRefresh = that.refreshPreview = function() { + previewManager.refresh(true); + }; + + forceRefresh(); + }; + + } + + // before: contains all the text in the input box BEFORE the selection. + // after: contains all the text in the input box AFTER the selection. + function Chunks() { } + + // startRegex: a regular expression to find the start tag + // endRegex: a regular expresssion to find the end tag + Chunks.prototype.findTags = function(startRegex, endRegex) { + + var chunkObj = this; + var regex; + + if(startRegex) { + + regex = util.extendRegExp(startRegex, "", "$"); + + this.before = this.before.replace(regex, + function(match) { + chunkObj.startTag = chunkObj.startTag + match; + return ""; + }); + + regex = util.extendRegExp(startRegex, "^", ""); + + this.selection = this.selection.replace(regex, + function(match) { + chunkObj.startTag = chunkObj.startTag + match; + return ""; + }); + } + + if(endRegex) { + + regex = util.extendRegExp(endRegex, "", "$"); + + this.selection = this.selection.replace(regex, + function(match) { + chunkObj.endTag = match + chunkObj.endTag; + return ""; + }); + + regex = util.extendRegExp(endRegex, "^", ""); + + this.after = this.after.replace(regex, + function(match) { + chunkObj.endTag = match + chunkObj.endTag; + return ""; + }); + } + }; + + // If remove is false, the whitespace is transferred + // to the before/after regions. + // + // If remove is true, the whitespace disappears. + Chunks.prototype.trimWhitespace = function(remove) { + var beforeReplacer, afterReplacer, that = this; + if(remove) { + beforeReplacer = afterReplacer = ""; + } else { + beforeReplacer = function(s) { + that.before += s; + return ""; + } + afterReplacer = function(s) { + that.after = s + that.after; + return ""; + } + } + + this.selection = this.selection.replace(/^(\s*)/, beforeReplacer).replace(/(\s*)$/, afterReplacer); + }; + + + Chunks.prototype.skipLines = function(nLinesBefore, nLinesAfter, findExtraNewlines) { + + if(nLinesBefore === undefined) { + nLinesBefore = 1; + } + + if(nLinesAfter === undefined) { + nLinesAfter = 1; + } + + nLinesBefore++; + nLinesAfter++; + + var regexText; + var replacementText; + + // chrome bug ... documented at: http://meta.stackexchange.com/questions/63307/blockquote-glitch-in-editor-in-chrome-6-and-7/65985#65985 + if(navigator.userAgent.match(/Chrome/)) { + "X".match(/()./); + } + + this.selection = this.selection.replace(/(^\n*)/, ""); + + this.startTag = this.startTag + re.$1; + + this.selection = this.selection.replace(/(\n*$)/, ""); + this.endTag = this.endTag + re.$1; + this.startTag = this.startTag.replace(/(^\n*)/, ""); + this.before = this.before + re.$1; + this.endTag = this.endTag.replace(/(\n*$)/, ""); + this.after = this.after + re.$1; + + if(this.before) { + + regexText = replacementText = ""; + + while(nLinesBefore--) { + regexText += "\\n?"; + replacementText += "\n"; + } + + if(findExtraNewlines) { + regexText = "\\n*"; + } + this.before = this.before.replace(new re(regexText + "$", ""), replacementText); + } + + if(this.after) { + + regexText = replacementText = ""; + + while(nLinesAfter--) { + regexText += "\\n?"; + replacementText += "\n"; + } + if(findExtraNewlines) { + regexText = "\\n*"; + } + + this.after = this.after.replace(new re(regexText, ""), replacementText); + } + }; + + // end of Chunks + + // A collection of the important regions on the page. + // Cached so we don't have to keep traversing the DOM. + // Also holds ieCachedRange and ieCachedScrollTop, where necessary; working around + // this issue: + // Internet explorer has problems with CSS sprite buttons that use HTML + // lists. When you click on the background image "button", IE will + // select the non-existent link text and discard the selection in the + // textarea. The solution to this is to cache the textarea selection + // on the button's mousedown event and set a flag. In the part of the + // code where we need to grab the selection, we check for the flag + // and, if it's set, use the cached area instead of querying the + // textarea. + // + // This ONLY affects Internet Explorer (tested on versions 6, 7 + // and 8) and ONLY on button clicks. Keyboard shortcuts work + // normally since the focus never leaves the textarea. + function PanelCollection(postfix) { + this.buttonBar = doc.getElementById("wmd-button-bar" + postfix); + this.preview = doc.getElementById("wmd-preview" + postfix); + this.input = doc.getElementById("wmd-input" + postfix); + } + ; + + // Returns true if the DOM element is visible, false if it's hidden. + // Checks if display is anything other than none. + util.isVisible = function(elem) { + + if(window.getComputedStyle) { + // Most browsers + return window.getComputedStyle(elem, null).getPropertyValue("display") !== "none"; + } else if(elem.currentStyle) { + // IE + return elem.currentStyle["display"] !== "none"; + } + }; + + + // Adds a listener callback to a DOM element which is fired on a specified + // event. + util.addEvent = function(elem, event, listener) { + if(elem.attachEvent) { + // IE only. The "on" is mandatory. + elem.attachEvent("on" + event, listener); + } else { + // Other browsers. + elem.addEventListener(event, listener, false); + } + }; + + + // Removes a listener callback from a DOM element which is fired on a specified + // event. + util.removeEvent = function(elem, event, listener) { + if(elem.detachEvent) { + // IE only. The "on" is mandatory. + elem.detachEvent("on" + event, listener); + } else { + // Other browsers. + elem.removeEventListener(event, listener, false); + } + }; + + // Converts \r\n and \r to \n. + util.fixEolChars = function(text) { + text = text.replace(/\r\n/g, "\n"); + text = text.replace(/\r/g, "\n"); + return text; + }; + + // Extends a regular expression. Returns a new RegExp + // using pre + regex + post as the expression. + // Used in a few functions where we have a base + // expression and we want to pre- or append some + // conditions to it (e.g. adding "$" to the end). + // The flags are unchanged. + // + // regex is a RegExp, pre and post are strings. + util.extendRegExp = function(regex, pre, post) { + + if(pre === null || pre === undefined) { + pre = ""; + } + if(post === null || post === undefined) { + post = ""; + } + + var pattern = regex.toString(); + var flags; + + // Replace the flags with empty space and store them. + pattern = pattern.replace(/\/([gim]*)$/, function(wholeMatch, flagsPart) { + flags = flagsPart; + return ""; + }); + + // Remove the slash delimiters on the regular expression. + pattern = pattern.replace(/(^\/|\/$)/g, ""); + pattern = pre + pattern + post; + + return new re(pattern, flags); + } + + // UNFINISHED + // The assignment in the while loop makes jslint cranky. + // I'll change it to a better loop later. + position.getTop = function(elem, isInner) { + var result = elem.offsetTop; + if(!isInner) { + while(elem = elem.offsetParent) { + result += elem.offsetTop; + } + } + return result; + }; + + position.getHeight = function(elem) { + return elem.offsetHeight || elem.scrollHeight; + }; + + position.getWidth = function(elem) { + return elem.offsetWidth || elem.scrollWidth; + }; + + position.getPageSize = function() { + + var scrollWidth, scrollHeight; + var innerWidth, innerHeight; + + // It's not very clear which blocks work with which browsers. + if(self.innerHeight && self.scrollMaxY) { + scrollWidth = doc.body.scrollWidth; + scrollHeight = self.innerHeight + self.scrollMaxY; + } else if(doc.body.scrollHeight > doc.body.offsetHeight) { + scrollWidth = doc.body.scrollWidth; + scrollHeight = doc.body.scrollHeight; + } else { + scrollWidth = doc.body.offsetWidth; + scrollHeight = doc.body.offsetHeight; + } + + if(self.innerHeight) { + // Non-IE browser + innerWidth = self.innerWidth; + innerHeight = self.innerHeight; + } else if(doc.documentElement && doc.documentElement.clientHeight) { + // Some versions of IE (IE 6 w/ a DOCTYPE declaration) + innerWidth = doc.documentElement.clientWidth; + innerHeight = doc.documentElement.clientHeight; + } else if(doc.body) { + // Other versions of IE + innerWidth = doc.body.clientWidth; + innerHeight = doc.body.clientHeight; + } + + var maxWidth = Math.max(scrollWidth, innerWidth); + var maxHeight = Math.max(scrollHeight, innerHeight); + return [maxWidth, maxHeight, innerWidth, innerHeight]; + }; + + // Handles pushing and popping TextareaStates for undo/redo commands. + // I should rename the stack variables to list. + function UndoManager(callback, panels) { + + var undoObj = this; + var undoStack = []; // A stack of undo states + var stackPtr = 0; // The index of the current state + var mode = "none"; + var lastState; // The last state + var timer; // The setTimeout handle for cancelling the timer + var inputStateObj; + + // Set the mode for later logic steps. + var setMode = function(newMode, noSave) { + if(mode != newMode) { + mode = newMode; + if(!noSave) { + saveState(); + } + } + + if(!uaSniffed.isIE || mode != "moving") { + timer = setTimeout(refreshState, 1); + } else { + inputStateObj = null; + } + }; + + var refreshState = function(isInitialState) { + inputStateObj = new ContenteditableState(panels, isInitialState); + timer = undefined; + }; + + this.setCommandMode = function() { + mode = "command"; + saveState(); + timer = setTimeout(refreshState, 0); + }; + + this.canUndo = function() { + return stackPtr > 1; + }; + + this.canRedo = function() { + if(undoStack[stackPtr + 1]) { + return true; + } + return false; + }; + + // Removes the last state and restores it. + this.undo = function() { + + if(undoObj.canUndo()) { + if(lastState) { + // What about setting state -1 to null or checking for undefined? + lastState.restore(); + lastState = null; + } else { + undoStack[stackPtr] = new ContenteditableState(panels); + undoStack[--stackPtr].restore(); + + if(callback) { + callback(); + } + } + } + + mode = "none"; + panels.input.focus(); + refreshState(); + }; + + // Redo an action. + this.redo = function() { + + if(undoObj.canRedo()) { + + undoStack[++stackPtr].restore(); + + if(callback) { + callback(); + } + } + + mode = "none"; + panels.input.focus(); + refreshState(); + }; + + // Push the input area state to the stack. + var saveState = function() { + var currState = inputStateObj || new ContenteditableState(panels); + + if(!currState) { + return false; + } + if(mode == "moving") { + if(!lastState) { + lastState = currState; + } + return; + } + if(lastState) { + if(undoStack[stackPtr - 1].text != lastState.text) { + undoStack[stackPtr++] = lastState; + } + lastState = null; + } + undoStack[stackPtr++] = currState; + undoStack[stackPtr + 1] = null; + if(callback) { + callback(); + } + }; + + var handleCtrlYZ = function(event) { + + var handled = false; + + if((event.ctrlKey || event.metaKey) && !event.altKey) { + + // IE and Opera do not support charCode. + var keyCode = event.charCode || event.keyCode; + var keyCodeChar = String.fromCharCode(keyCode); + + switch(keyCodeChar.toLowerCase()) { + + case "y": + undoObj.redo(); + handled = true; + break; + + case "z": + if(!event.shiftKey) { + undoObj.undo(); + } else { + undoObj.redo(); + } + handled = true; + break; + } + } + + if(handled) { + if(event.preventDefault) { + event.preventDefault(); + } + if(window.event) { + window.event.returnValue = false; + } + return; + } + }; + + // Set the mode depending on what is going on in the input area. + var handleModeChange = function(event) { + + if(!event.ctrlKey && !event.metaKey) { + + var keyCode = event.keyCode; + + if((keyCode >= 33 && keyCode <= 40) || (keyCode >= 63232 && keyCode <= 63235)) { + // 33 - 40: page up/dn and arrow keys + // 63232 - 63235: page up/dn and arrow keys on safari + setMode("moving"); + } else if(keyCode == 8 || keyCode == 46 || keyCode == 127) { + // 8: backspace + // 46: delete + // 127: delete + setMode("deleting"); + } else if(keyCode == 13) { + // 13: Enter + setMode("newlines"); + } else if(keyCode == 27) { + // 27: escape + setMode("escape"); + } else if((keyCode < 16 || keyCode > 20) && keyCode != 91) { + // 16-20 are shift, etc. + // 91: left window key + // I think this might be a little messed up since there are + // a lot of nonprinting keys above 20. + setMode("typing"); + } + } + }; + + var setEventHandlers = function() { + util.addEvent(panels.input, "keypress", function(event) { + // keyCode 89: y + // keyCode 90: z + if((event.ctrlKey || event.metaKey) && !event.altKey && (event.keyCode == 89 || event.keyCode == 90)) { + event.preventDefault(); + } + }); + + util.addEvent(panels.input, "focusout", function(event) { + + var selection = window.getSelection(); + $(panels.input).data('selection', { + start: { + node: selection.anchorNode, + offset: selection.anchorOffset + }, + end: { + node: selection.focusNode, + offset: selection.focusOffset, + } + }); + }); + + var handlePaste = function() { + if(uaSniffed.isIE || (inputStateObj && inputStateObj.text != panels.input.value)) { + if(timer == undefined) { + mode = "paste"; + saveState(); + refreshState(); + } + } + }; + + util.addEvent(panels.input, "keydown", handleCtrlYZ); + util.addEvent(panels.input, "keydown", handleModeChange); + util.addEvent(panels.input, "mousedown", function() { + setMode("moving"); + }); + + panels.input.onpaste = handlePaste; + panels.input.ondrop = handlePaste; + }; + + var init = function() { + setEventHandlers(); + refreshState(true); + saveState(); + }; + + init(); + } + + // end of UndoManager + + // The input textarea state/contents. + // This is used to implement undo/redo by the undo manager. + function TextareaState(panels, isInitialState) { + + // Aliases + var stateObj = this; + var inputArea = panels.input; + this.init = function() { + if(!util.isVisible(inputArea)) { + return; + } + if(!isInitialState && doc.activeElement && doc.activeElement !== inputArea) { // this happens when tabbing out of the input box + return; + } + + this.setInputAreaSelectionStartEnd(); + this.scrollTop = inputArea.scrollTop; + if(!this.text && inputArea.selectionStart || inputArea.selectionStart === 0) { + this.text = inputArea.value; + } + + } + + // Sets the selected text in the input box after we've performed an + // operation. + this.setInputAreaSelection = function() { + + if(!util.isVisible(inputArea)) { + return; + } + + if(inputArea.selectionStart !== undefined && !uaSniffed.isOpera) { + + inputArea.focus(); + inputArea.selectionStart = stateObj.start; + inputArea.selectionEnd = stateObj.end; + inputArea.scrollTop = stateObj.scrollTop; + } else if(doc.selection) { + + if(doc.activeElement && doc.activeElement !== inputArea) { + return; + } + + inputArea.focus(); + var range = inputArea.createTextRange(); + range.moveStart("character", -inputArea.value.length); + range.moveEnd("character", -inputArea.value.length); + range.moveEnd("character", stateObj.end); + range.moveStart("character", stateObj.start); + range.select(); + } + }; + + this.setInputAreaSelectionStartEnd = function() { + + if(!panels.ieCachedRange && (inputArea.selectionStart || inputArea.selectionStart === 0)) { + + stateObj.start = inputArea.selectionStart; + stateObj.end = inputArea.selectionEnd; + } else if(doc.selection) { + + stateObj.text = util.fixEolChars(inputArea.value); + + // IE loses the selection in the textarea when buttons are + // clicked. On IE we cache the selection. Here, if something is cached, + // we take it. + var range = panels.ieCachedRange || doc.selection.createRange(); + + var fixedRange = util.fixEolChars(range.text); + var marker = "\x07"; + var markedRange = marker + fixedRange + marker; + range.text = markedRange; + var inputText = util.fixEolChars(inputArea.value); + + range.moveStart("character", -markedRange.length); + range.text = fixedRange; + + stateObj.start = inputText.indexOf(marker); + stateObj.end = inputText.lastIndexOf(marker) - marker.length; + + var len = stateObj.text.length - util.fixEolChars(inputArea.value).length; + + if(len) { + range.moveStart("character", -fixedRange.length); + while(len--) { + fixedRange += "\n"; + stateObj.end += 1; + } + range.text = fixedRange; + } + + if(panels.ieCachedRange) + stateObj.scrollTop = panels.ieCachedScrollTop; // this is set alongside with ieCachedRange + + panels.ieCachedRange = null; + + this.setInputAreaSelection(); + } + }; + + // Restore this state into the input area. + this.restore = function() { + + if(stateObj.text != undefined && stateObj.text != inputArea.value) { + inputArea.value = stateObj.text; + } + this.setInputAreaSelection(); + inputArea.scrollTop = stateObj.scrollTop; + }; + + // Gets a collection of HTML chunks from the inptut textarea. + this.getChunks = function() { + + var chunk = new Chunks(); + chunk.before = util.fixEolChars(stateObj.text.substring(0, stateObj.start)); + chunk.startTag = ""; + chunk.selection = util.fixEolChars(stateObj.text.substring(stateObj.start, stateObj.end)); + chunk.endTag = ""; + chunk.after = util.fixEolChars(stateObj.text.substring(stateObj.end)); + chunk.scrollTop = stateObj.scrollTop; + + return chunk; + }; + + // Sets the TextareaState properties given a chunk of markdown. + this.setChunks = function(chunk) { + + chunk.before = chunk.before + chunk.startTag; + chunk.after = chunk.endTag + chunk.after; + + this.start = chunk.before.length; + this.end = chunk.before.length + chunk.selection.length; + this.text = chunk.before + chunk.selection + chunk.after; + this.scrollTop = chunk.scrollTop; + }; + this.init(); + } + ; + + function ContenteditableState(panels, isInitialState) { + + // Aliases + var stateObj = this; + var inputArea = panels.input; + + this.init = function() { + if(!util.isVisible(inputArea)) { + return; + } + if(!isInitialState && doc.activeElement && doc.activeElement !== inputArea) { // this happens when tabbing out of the input box + return; + } + + this.setInputAreaSelectionStartEnd(); + this.scrollTop = inputArea.scrollTop; + if(!this.text && inputArea.selectionStart || inputArea.selectionStart === 0) { + this.text = inputArea.value; + } + + } + + // Sets the selected text in the input box after we've performed an + // operation. + this.setInputAreaSelection = function() { + + if(!util.isVisible(inputArea)) { + return; + } + + if(inputArea.selectionStart !== undefined && !uaSniffed.isOpera) { + + inputArea.focus(); + inputArea.selectionStart = stateObj.start; + inputArea.selectionEnd = stateObj.end; + inputArea.scrollTop = stateObj.scrollTop; + } else if(doc.selection) { + + if(doc.activeElement && doc.activeElement !== inputArea) { + return; + } + + inputArea.focus(); + var range = inputArea.createTextRange(); + range.moveStart("character", -inputArea.value.length); + range.moveEnd("character", -inputArea.value.length); + range.moveEnd("character", stateObj.end); + range.moveStart("character", stateObj.start); + range.select(); + } + }; + + this.setInputAreaSelectionStartEnd = function() { + console.log($(panels.input).data('selection')); + this.selection = $(panels.input).data('selection'); + + return; + + + if(!panels.ieCachedRange && (inputArea.selectionStart || inputArea.selectionStart === 0)) { + + stateObj.start = inputArea.selectionStart; + stateObj.end = inputArea.selectionEnd; + } else if(doc.selection) { + debugger; + console.log(doc.selection); + stateObj.text = util.fixEolChars(inputArea.value); + + // IE loses the selection in the textarea when buttons are + // clicked. On IE we cache the selection. Here, if something is cached, + // we take it. + var range = panels.ieCachedRange || doc.selection.createRange(); + + var fixedRange = util.fixEolChars(range.text); + var marker = "\x07"; + var markedRange = marker + fixedRange + marker; + range.text = markedRange; + var inputText = util.fixEolChars(inputArea.value); + + range.moveStart("character", -markedRange.length); + range.text = fixedRange; + + stateObj.start = inputText.indexOf(marker); + stateObj.end = inputText.lastIndexOf(marker) - marker.length; + + var len = stateObj.text.length - util.fixEolChars(inputArea.value).length; + + if(len) { + range.moveStart("character", -fixedRange.length); + while(len--) { + fixedRange += "\n"; + stateObj.end += 1; + } + range.text = fixedRange; + } + + if(panels.ieCachedRange) + stateObj.scrollTop = panels.ieCachedScrollTop; // this is set alongside with ieCachedRange + + panels.ieCachedRange = null; + + this.setInputAreaSelection(); + } + }; + + // Restore this state into the input area. + this.restore = function() { + + if(stateObj.text != undefined && stateObj.text != inputArea.value) { + inputArea.value = stateObj.text; + } + this.setInputAreaSelection(); + inputArea.scrollTop = stateObj.scrollTop; + }; + + // Gets a collection of HTML chunks from the inptut textarea. + this.getChunks = function() { + + var chunk = new Chunks(); + chunk.before = util.fixEolChars(stateObj.text.substring(0, stateObj.start)); + chunk.startTag = ""; + chunk.selection = util.fixEolChars(stateObj.text.substring(stateObj.start, stateObj.end)); + chunk.endTag = ""; + chunk.after = util.fixEolChars(stateObj.text.substring(stateObj.end)); + chunk.scrollTop = stateObj.scrollTop; + + return chunk; + }; + + // Sets the TextareaState properties given a chunk of markdown. + this.setChunks = function(chunk) { + + chunk.before = chunk.before + chunk.startTag; + chunk.after = chunk.endTag + chunk.after; + + this.start = chunk.before.length; + this.end = chunk.before.length + chunk.selection.length; + this.text = chunk.before + chunk.selection + chunk.after; + this.scrollTop = chunk.scrollTop; + }; + this.init(); + } + ; + + function PreviewManager(converter, panels, previewRefreshCallback) { + + var managerObj = this; + var timeout; + var elapsedTime; + var oldInputText; + var maxDelay = 3000; + var startType = "delayed"; // The other legal value is "manual" + + // Adds event listeners to elements + var setupEvents = function(inputElem, listener) { + + util.addEvent(inputElem, "input", listener); + inputElem.onpaste = listener; + inputElem.ondrop = listener; + + util.addEvent(inputElem, "keypress", listener); + util.addEvent(inputElem, "keydown", listener); + }; + + var getDocScrollTop = function() { + + var result = 0; + + if(window.innerHeight) { + result = window.pageYOffset; + } else + if(doc.documentElement && doc.documentElement.scrollTop) { + result = doc.documentElement.scrollTop; + } else + if(doc.body) { + result = doc.body.scrollTop; + } + + return result; + }; + + var makePreviewHtml = function() { + + // If there is no registered preview panel + // there is nothing to do. + if(!panels.preview) + return; + + + var text = panels.input.value; + if(text && text == oldInputText) { + return; // Input text hasn't changed. + } else { + oldInputText = text; + } + + var prevTime = new Date().getTime(); + + text = converter.makeHtml(text); + + // Calculate the processing time of the HTML creation. + // It's used as the delay time in the event listener. + var currTime = new Date().getTime(); + elapsedTime = currTime - prevTime; + + pushPreviewHtml(text); + }; + + // setTimeout is already used. Used as an event listener. + var applyTimeout = function() { + + if(timeout) { + clearTimeout(timeout); + timeout = undefined; + } + + if(startType !== "manual") { + + var delay = 0; + + if(startType === "delayed") { + delay = elapsedTime; + } + + if(delay > maxDelay) { + delay = maxDelay; + } + timeout = setTimeout(makePreviewHtml, delay); + } + }; + + var getScaleFactor = function(panel) { + if(panel.scrollHeight <= panel.clientHeight) { + return 1; + } + return panel.scrollTop / (panel.scrollHeight - panel.clientHeight); + }; + + var setPanelScrollTops = function() { + if(panels.preview) { + panels.preview.scrollTop = (panels.preview.scrollHeight - panels.preview.clientHeight) * getScaleFactor(panels.preview); + } + }; + + this.refresh = function(requiresRefresh) { + + if(requiresRefresh) { + oldInputText = ""; + makePreviewHtml(); + } else { + applyTimeout(); + } + }; + + this.processingTime = function() { + return elapsedTime; + }; + + var isFirstTimeFilled = true; + + // IE doesn't let you use innerHTML if the element is contained somewhere in a table + // (which is the case for inline editing) -- in that case, detach the element, set the + // value, and reattach. Yes, that *is* ridiculous. + var ieSafePreviewSet = function(text) { + var preview = panels.preview; + var parent = preview.parentNode; + var sibling = preview.nextSibling; + parent.removeChild(preview); + preview.innerHTML = text; + if(!sibling) + parent.appendChild(preview); + else + parent.insertBefore(preview, sibling); + } + + var nonSuckyBrowserPreviewSet = function(text) { + panels.preview.innerHTML = text; + } + + var previewSetter; + + var previewSet = function(text) { + if(previewSetter) + return previewSetter(text); + + try { + nonSuckyBrowserPreviewSet(text); + previewSetter = nonSuckyBrowserPreviewSet; + } catch(e) { + previewSetter = ieSafePreviewSet; + previewSetter(text); + } + }; + + var pushPreviewHtml = function(text) { + + var emptyTop = position.getTop(panels.input) - getDocScrollTop(); + + if(panels.preview) { + previewSet(text); + previewRefreshCallback(); + } + + setPanelScrollTops(); + + if(isFirstTimeFilled) { + isFirstTimeFilled = false; + return; + } + + var fullTop = position.getTop(panels.input) - getDocScrollTop(); + + if(uaSniffed.isIE) { + setTimeout(function() { + window.scrollBy(0, fullTop - emptyTop); + }, 0); + } else { + window.scrollBy(0, fullTop - emptyTop); + } + }; + + var init = function() { + + setupEvents(panels.input, applyTimeout); + makePreviewHtml(); + + if(panels.preview) { + panels.preview.scrollTop = 0; + } + }; + + init(); + } + ; + + // Creates the background behind the hyperlink text entry box. + // And download dialog + // Most of this has been moved to CSS but the div creation and + // browser-specific hacks remain here. + ui.createBackground = function() { + + var background = doc.createElement("div"), + style = background.style; + + background.className = "wmd-prompt-background"; + + style.position = "absolute"; + style.top = "0"; + + style.zIndex = "1000"; + + if(uaSniffed.isIE) { + style.filter = "alpha(opacity=50)"; + } else { + style.opacity = "0.5"; + } + + var pageSize = position.getPageSize(); + style.height = pageSize[1] + "px"; + + if(uaSniffed.isIE) { + style.left = doc.documentElement.scrollLeft; + style.width = doc.documentElement.clientWidth; + } else { + style.left = "0"; + style.width = "100%"; + } + + doc.body.appendChild(background); + return background; + }; + + // This simulates a modal dialog box and asks for the URL when you + // click the hyperlink or image buttons. + // + // text: The html for the input box. + // defaultInputText: The default value that appears in the input box. + // callback: The function which is executed when the prompt is dismissed, either via OK or Cancel. + // It receives a single argument; either the entered text (if OK was chosen) or null (if Cancel + // was chosen). + ui.prompt = function(text, defaultInputText, callback) { + + // These variables need to be declared at this level since they are used + // in multiple functions. + var dialog; // The dialog box. + var input; // The text box where you enter the hyperlink. + + + if(defaultInputText === undefined) { + defaultInputText = ""; + } + + // Used as a keydown event handler. Esc dismisses the prompt. + // Key code 27 is ESC. + var checkEscape = function(key) { + var code = (key.charCode || key.keyCode); + if(code === 27) { + if(key.stopPropagation) + key.stopPropagation(); + close(true); + return false; + } + }; + + // Dismisses the hyperlink input box. + // isCancel is true if we don't care about the input text. + // isCancel is false if we are going to keep the text. + var close = function(isCancel) { + util.removeEvent(doc.body, "keyup", checkEscape); + var text = input.value; + + if(isCancel) { + text = null; + } else { + // Fixes common pasting errors. + text = text.replace(/^http:\/\/(https?|ftp):\/\//, '$1://'); + if(!/^(?:https?|ftp):\/\//.test(text)) + text = 'http://' + text; + } + + dialog.parentNode.removeChild(dialog); + + callback(text); + return false; + }; + + + + // Create the text input box form/window. + var createDialog = function() { + + // The main dialog box. + dialog = doc.createElement("div"); + dialog.className = "wmd-prompt-dialog"; + dialog.style.padding = "10px;"; + dialog.style.position = "fixed"; + dialog.style.width = "400px"; + dialog.style.zIndex = "1001"; + + // The dialog text. + var question = doc.createElement("div"); + question.innerHTML = text; + question.style.padding = "5px"; + dialog.appendChild(question); + + // The web form container for the text box and buttons. + var form = doc.createElement("form"), + style = form.style; + form.onsubmit = function() { + return close(false); + }; + style.padding = "0"; + style.margin = "0"; + style.cssFloat = "left"; + style.width = "100%"; + style.textAlign = "center"; + style.position = "relative"; + dialog.appendChild(form); + + // The input text box + input = doc.createElement("input"); + input.type = "text"; + input.value = defaultInputText; + style = input.style; + style.display = "block"; + style.width = "80%"; + style.marginLeft = style.marginRight = "auto"; + form.appendChild(input); + + // The ok button + var okButton = doc.createElement("input"); + okButton.type = "button"; + okButton.onclick = function() { + return close(false); + }; + okButton.value = "OK"; + style = okButton.style; + style.margin = "10px"; + style.display = "inline"; + style.width = "7em"; + + + // The cancel button + var cancelButton = doc.createElement("input"); + cancelButton.type = "button"; + cancelButton.onclick = function() { + return close(true); + }; + cancelButton.value = "Cancel"; + style = cancelButton.style; + style.margin = "10px"; + style.display = "inline"; + style.width = "7em"; + + form.appendChild(okButton); + form.appendChild(cancelButton); + + util.addEvent(doc.body, "keyup", checkEscape); + dialog.style.top = "50%"; + dialog.style.left = "50%"; + dialog.style.display = "block"; + if(uaSniffed.isIE_5or6) { + dialog.style.position = "absolute"; + dialog.style.top = doc.documentElement.scrollTop + 200 + "px"; + dialog.style.left = "50%"; + } + doc.body.appendChild(dialog); + + // This has to be done AFTER adding the dialog to the form if you + // want it to be centered. + dialog.style.marginTop = -(position.getHeight(dialog) / 2) + "px"; + dialog.style.marginLeft = -(position.getWidth(dialog) / 2) + "px"; + + }; + + // Why is this in a zero-length timeout? + // Is it working around a browser bug? + setTimeout(function() { + + createDialog(); + + var defTextLen = defaultInputText.length; + if(input.selectionStart !== undefined) { + input.selectionStart = 0; + input.selectionEnd = defTextLen; + } else if(input.createTextRange) { + var range = input.createTextRange(); + range.collapse(false); + range.moveStart("character", -defTextLen); + range.moveEnd("character", defTextLen); + range.select(); + } + + input.focus(); + }, 0); + }; + + function UIManager(postfix, panels, undoManager, previewManager, commandManager, helpOptions, getString) { + + var inputBox = panels.input, + buttons = {}; // buttons.undo, buttons.link, etc. The actual DOM elements. + + makeSpritedButtonRow(); + + var keyEvent = "keydown"; + if(uaSniffed.isOpera) { + keyEvent = "keypress"; + } + + util.addEvent(inputBox, keyEvent, function(key) { + + // Check to see if we have a button key and, if so execute the callback. + if((key.ctrlKey || key.metaKey) && !key.altKey && !key.shiftKey) { + + var keyCode = key.charCode || key.keyCode; + var keyCodeStr = String.fromCharCode(keyCode).toLowerCase(); + + switch(keyCodeStr) { + case "b": + doClick(buttons.bold); + break; + case "i": + doClick(buttons.italic); + break; + case "l": + doClick(buttons.link); + break; + case "q": + doClick(buttons.quote); + break; + case "k": + doClick(buttons.code); + break; + case "g": + doClick(buttons.image); + break; + case "o": + doClick(buttons.olist); + break; + case "u": + doClick(buttons.ulist); + break; + case "h": + doClick(buttons.heading); + break; + case "r": + doClick(buttons.hr); + break; + case "y": + doClick(buttons.redo); + break; + case "z": + if(key.shiftKey) { + doClick(buttons.redo); + } else { + doClick(buttons.undo); + } + break; + default: + return; + } + + + if(key.preventDefault) { + key.preventDefault(); + } + + if(window.event) { + window.event.returnValue = false; + } + } + }); + + // Auto-indent on shift-enter + util.addEvent(inputBox, "keyup", function(key) { + if(key.shiftKey && !key.ctrlKey && !key.metaKey) { + var keyCode = key.charCode || key.keyCode; + // Character 13 is Enter + if(keyCode === 13) { + var fakeButton = {}; + fakeButton.textOp = bindCommand("doAutoindent"); + doClick(fakeButton); + } + } + }); + + // special handler because IE clears the context of the textbox on ESC + if(uaSniffed.isIE) { + util.addEvent(inputBox, "keydown", function(key) { + var code = key.keyCode; + if(code === 27) { + return false; + } + }); + } + + + // Perform the button's action. + function doClick(button) { + + inputBox.focus(); + + if(button.textOp) { + + if(undoManager) { + undoManager.setCommandMode(); + } + + var state = new ContenteditableState(panels); + + if(!state) { + return; + } + + var chunks = state.getChunks(); + + // Some commands launch a "modal" prompt dialog. Javascript + // can't really make a modal dialog box and the WMD code + // will continue to execute while the dialog is displayed. + // This prevents the dialog pattern I'm used to and means + // I can't do something like this: + // + // var link = CreateLinkDialog(); + // makeMarkdownLink(link); + // + // Instead of this straightforward method of handling a + // dialog I have to pass any code which would execute + // after the dialog is dismissed (e.g. link creation) + // in a function parameter. + // + // Yes this is awkward and I think it sucks, but there's + // no real workaround. Only the image and link code + // create dialogs and require the function pointers. + var fixupInputArea = function() { + + inputBox.focus(); + + if(chunks) { + state.setChunks(chunks); + } + + state.restore(); + previewManager.refresh(); + }; + + var noCleanup = button.textOp(chunks, fixupInputArea); + + if(!noCleanup) { + fixupInputArea(); + } + + } + + if(button.execute) { + button.execute(undoManager); + } + } + ; + + function setupButton(button, isEnabled) { + + var normalYShift = "0px"; + var disabledYShift = "-20px"; + var highlightYShift = "-40px"; + var image = button.getElementsByTagName("span")[0]; + if(isEnabled) { + image.style.backgroundPosition = button.XShift + " " + normalYShift; + button.onmouseover = function() { + image.style.backgroundPosition = this.XShift + " " + highlightYShift; + }; + + button.onmouseout = function() { + image.style.backgroundPosition = this.XShift + " " + normalYShift; + }; + + // IE tries to select the background image "button" text (it's + // implemented in a list item) so we have to cache the selection + // on mousedown. + if(uaSniffed.isIE) { + button.onmousedown = function() { + if(doc.activeElement && doc.activeElement !== panels.input) { // we're not even in the input box, so there's no selection + return; + } + panels.ieCachedRange = document.selection.createRange(); + panels.ieCachedScrollTop = panels.input.scrollTop; + }; + } + + if(!button.isHelp) { + button.onclick = function() { + if(this.onmouseout) { + this.onmouseout(); + } + doClick(this); + return false; + } + } + } else { + image.style.backgroundPosition = button.XShift + " " + disabledYShift; + button.onmouseover = button.onmouseout = button.onclick = function() { }; + } + } + + function bindCommand(method) { + if(typeof method === "string") + method = commandManager[method]; + return function() { + method.apply(commandManager, arguments); + } + } + + function makeSpritedButtonRow() { + + var buttonBar = panels.buttonBar; + + var normalYShift = "0px"; + var disabledYShift = "-20px"; + var highlightYShift = "-40px"; + + var buttonRow = document.createElement("ul"); + buttonRow.id = "wmd-button-row" + postfix; + buttonRow.className = 'wmd-button-row'; + buttonRow = buttonBar.appendChild(buttonRow); + var xPosition = 0; + var makeButton = function(id, title, XShift, textOp) { + var button = document.createElement("li"); + button.className = "wmd-button"; + button.style.left = xPosition + "px"; + xPosition += 25; + var buttonImage = document.createElement("span"); + button.id = id + postfix; + button.appendChild(buttonImage); + button.title = title; + button.XShift = XShift; + if(textOp) + button.textOp = textOp; + setupButton(button, true); + buttonRow.appendChild(button); + return button; + }; + var makeSpacer = function(num) { + var spacer = document.createElement("li"); + spacer.className = "wmd-spacer wmd-spacer" + num; + spacer.id = "wmd-spacer" + num + postfix; + buttonRow.appendChild(spacer); + xPosition += 25; + } + + buttons.bold = makeButton("wmd-bold-button", getString("bold"), "0px", bindCommand("doBold")); + buttons.italic = makeButton("wmd-italic-button", getString("italic"), "-20px", bindCommand("doItalic")); + makeSpacer(1); + buttons.link = makeButton("wmd-link-button", getString("link"), "-40px", bindCommand(function(chunk, postProcessing) { + return this.doLinkOrImage(chunk, postProcessing, false); + })); + buttons.quote = makeButton("wmd-quote-button", getString("quote"), "-60px", bindCommand("doBlockquote")); + buttons.code = makeButton("wmd-code-button", getString("code"), "-80px", bindCommand("doCode")); + buttons.image = makeButton("wmd-image-button", getString("image"), "-100px", bindCommand(function(chunk, postProcessing) { + return this.doLinkOrImage(chunk, postProcessing, true); + })); + makeSpacer(2); + buttons.olist = makeButton("wmd-olist-button", getString("olist"), "-120px", bindCommand(function(chunk, postProcessing) { + this.doList(chunk, postProcessing, true); + })); + buttons.ulist = makeButton("wmd-ulist-button", getString("ulist"), "-140px", bindCommand(function(chunk, postProcessing) { + this.doList(chunk, postProcessing, false); + })); + buttons.heading = makeButton("wmd-heading-button", getString("heading"), "-160px", bindCommand("doHeading")); + buttons.hr = makeButton("wmd-hr-button", getString("hr"), "-180px", bindCommand("doHorizontalRule")); + makeSpacer(3); + buttons.undo = makeButton("wmd-undo-button", getString("undo"), "-200px", null); + buttons.undo.execute = function(manager) { + if(manager) + manager.undo(); + }; + + var redoTitle = /win/.test(nav.platform.toLowerCase()) ? + getString("redo") : + getString("redomac"); // mac and other non-Windows platforms + + buttons.redo = makeButton("wmd-redo-button", redoTitle, "-220px", null); + buttons.redo.execute = function(manager) { + if(manager) + manager.redo(); + }; + + if(helpOptions) { + var helpButton = document.createElement("li"); + var helpButtonImage = document.createElement("span"); + helpButton.appendChild(helpButtonImage); + helpButton.className = "wmd-button wmd-help-button"; + helpButton.id = "wmd-help-button" + postfix; + helpButton.XShift = "-240px"; + helpButton.isHelp = true; + helpButton.style.right = "0px"; + helpButton.title = getString("help"); + helpButton.onclick = helpOptions.handler; + + setupButton(helpButton, true); + buttonRow.appendChild(helpButton); + buttons.help = helpButton; + } + + setUndoRedoButtonStates(); + } + + function setUndoRedoButtonStates() { + if(undoManager) { + setupButton(buttons.undo, undoManager.canUndo()); + setupButton(buttons.redo, undoManager.canRedo()); + } + } + ; + + this.setUndoRedoButtonStates = setUndoRedoButtonStates; + + } + + function CommandManager(pluginHooks, getString, converter) { + this.hooks = pluginHooks; + this.getString = getString; + this.converter = converter; + } + + var commandProto = CommandManager.prototype; + + // The markdown symbols - 4 spaces = code, > = blockquote, etc. + commandProto.prefixes = "(?:\\s{4,}|\\s*>|\\s*-\\s+|\\s*\\d+\\.|=|\\+|-|_|\\*|#|\\s*\\[[^\n]]+\\]:)"; + + // Remove markdown symbols from the chunk selection. + commandProto.unwrap = function(chunk) { + var txt = new re("([^\\n])\\n(?!(\\n|" + this.prefixes + "))", "g"); + chunk.selection = chunk.selection.replace(txt, "$1 $2"); + }; + + commandProto.wrap = function(chunk, len) { + this.unwrap(chunk); + var regex = new re("(.{1," + len + "})( +|$\\n?)", "gm"), + that = this; + + chunk.selection = chunk.selection.replace(regex, function(line, marked) { + if(new re("^" + that.prefixes, "").test(line)) { + return line; + } + return marked + "\n"; + }); + + chunk.selection = chunk.selection.replace(/\s+$/, ""); + }; + + commandProto.doBold = function(chunk, postProcessing) { + return this.doBorI(chunk, postProcessing, 2, this.getString("boldexample")); + }; + + commandProto.doItalic = function(chunk, postProcessing) { + return this.doBorI(chunk, postProcessing, 1, this.getString("italicexample")); + }; + + // chunk: The selected region that will be enclosed with */** + // nStars: 1 for italics, 2 for bold + // insertText: If you just click the button without highlighting text, this gets inserted + commandProto.doBorI = function(chunk, postProcessing, nStars, insertText) { + + // Get rid of whitespace and fixup newlines. + chunk.trimWhitespace(); + chunk.selection = chunk.selection.replace(/\n{2,}/g, "\n"); + + // Look for stars before and after. Is the chunk already marked up? + // note that these regex matches cannot fail + var starsBefore = /(\**$)/.exec(chunk.before)[0]; + var starsAfter = /(^\**)/.exec(chunk.after)[0]; + + var prevStars = Math.min(starsBefore.length, starsAfter.length); + + // Remove stars if we have to since the button acts as a toggle. + if((prevStars >= nStars) && (prevStars != 2 || nStars != 1)) { + chunk.before = chunk.before.replace(re("[*]{" + nStars + "}$", ""), ""); + chunk.after = chunk.after.replace(re("^[*]{" + nStars + "}", ""), ""); + } else if(!chunk.selection && starsAfter) { + // It's not really clear why this code is necessary. It just moves + // some arbitrary stuff around. + chunk.after = chunk.after.replace(/^([*_]*)/, ""); + chunk.before = chunk.before.replace(/(\s?)$/, ""); + var whitespace = re.$1; + chunk.before = chunk.before + starsAfter + whitespace; + } else { + + // In most cases, if you don't have any selected text and click the button + // you'll get a selected, marked up region with the default text inserted. + if(!chunk.selection && !starsAfter) { + chunk.selection = insertText; + } + + // Add the true markup. + var markup = nStars <= 1 ? "*" : "**"; // shouldn't the test be = ? + chunk.before = chunk.before + markup; + chunk.after = markup + chunk.after; + } + + return; + }; + + commandProto.stripLinkDefs = function(text, defsToAdd) { + + text = text.replace(/^[ ]{0,3}\[(\d+)\]:[ \t]*\n?[ \t]*?[ \t]*\n?[ \t]*(?:(\n*)["(](.+?)[")][ \t]*)?(?:\n+|$)/gm, + function(totalMatch, id, link, newlines, title) { + defsToAdd[id] = totalMatch.replace(/\s*$/, ""); + if(newlines) { + // Strip the title and return that separately. + defsToAdd[id] = totalMatch.replace(/["(](.+?)[")]$/, ""); + return newlines + title; + } + return ""; + }); + + return text; + }; + + commandProto.addLinkDef = function(chunk, linkDef) { + + var refNumber = 0; // The current reference number + var defsToAdd = {}; // + // Start with a clean slate by removing all previous link definitions. + chunk.before = this.stripLinkDefs(chunk.before, defsToAdd); + chunk.selection = this.stripLinkDefs(chunk.selection, defsToAdd); + chunk.after = this.stripLinkDefs(chunk.after, defsToAdd); + + var defs = ""; + var regex = /(\[)((?:\[[^\]]*\]|[^\[\]])*)(\][ ]?(?:\n[ ]*)?\[)(\d+)(\])/g; + + // The above regex, used to update [foo][13] references after renumbering, + // is much too liberal; it can catch things that are not actually parsed + // as references (notably: code). It's impossible to know which matches are + // real references without performing a markdown conversion, so that's what + // we do. All matches are replaced with a unique reference number, which is + // given a unique link. The uniquifier in both cases is the character offset + // of the match inside the source string. The modified version is then sent + // through the Markdown renderer. Because link reference are stripped during + // rendering, the unique link is present in the rendered version if and only + // if the match at its offset was in fact rendered as a link or image. + var complete = chunk.before + chunk.selection + chunk.after; + var rendered = this.converter.makeHtml(complete); + var testlink = "http://this-is-a-real-link.biz/"; + + // If our fake link appears in the rendered version *before* we have added it, + // this probably means you're a Meta Stack Exchange user who is deliberately + // trying to break this feature. You can still break this workaround if you + // attach a plugin to the converter that sometimes (!) inserts this link. In + // that case, consider yourself unsupported. + while(rendered.indexOf(testlink) != - 1) + testlink += "nicetry/"; + + var fakedefs = "\n\n"; + + // the regex is tested on the (up to) three chunks separately, and on substrings, + // so in order to have the correct offsets to check against okayToModify(), we + // have to keep track of how many characters are in the original source before + // the substring that we're looking at. Note that doLinkOrImage aligns the selection + // on potential brackets, so there should be no major breakage from the chunk + // separation. + var skippedChars = 0; + + var uniquified = complete.replace(regex, function uniquify(wholeMatch, before, inner, afterInner, id, end, offset) { + skippedChars += offset; + fakedefs += " [" + skippedChars + "]: " + testlink + skippedChars + "/unicorn\n"; + skippedChars += before.length; + inner = inner.replace(regex, uniquify); + skippedChars -= before.length; + var result = before + inner + afterInner + skippedChars + end; + skippedChars -= offset; + return result; + }); + + rendered = this.converter.makeHtml(uniquified + fakedefs); + + var okayToModify = function(offset) { + return rendered.indexOf(testlink + offset + "/unicorn") !== -1; + } + + var addDefNumber = function(def) { + refNumber++; + def = def.replace(/^[ ]{0,3}\[(\d+)\]:/, " [" + refNumber + "]:"); + defs += "\n" + def; + }; + + // note that + // a) the recursive call to getLink cannot go infinite, because by definition + // of regex, inner is always a proper substring of wholeMatch, and + // b) more than one level of nesting is neither supported by the regex + // nor making a lot of sense (the only use case for nesting is a linked image) + var getLink = function(wholeMatch, before, inner, afterInner, id, end, offset) { + if(!okayToModify(skippedChars + offset)) + return wholeMatch; + skippedChars += offset + before.length; + inner = inner.replace(regex, getLink); + skippedChars -= offset + before.length; + if(defsToAdd[id]) { + addDefNumber(defsToAdd[id]); + return before + inner + afterInner + refNumber + end; + } + return wholeMatch; + }; + + var len = chunk.before.length; + chunk.before = chunk.before.replace(regex, getLink); + skippedChars += len; + + len = chunk.selection.length; + if(linkDef) { + addDefNumber(linkDef); + } else { + chunk.selection = chunk.selection.replace(regex, getLink); + } + skippedChars += len; + + var refOut = refNumber; + + chunk.after = chunk.after.replace(regex, getLink); + + if(chunk.after) { + chunk.after = chunk.after.replace(/\n*$/, ""); + } + if(!chunk.after) { + chunk.selection = chunk.selection.replace(/\n*$/, ""); + } + + chunk.after += "\n\n" + defs; + + return refOut; + }; + + // takes the line as entered into the add link/as image dialog and makes + // sure the URL and the optinal title are "nice". + function properlyEncoded(linkdef) { + return linkdef.replace(/^\s*(.*?)(?:\s+"(.+)")?\s*$/, function(wholematch, link, title) { + + var inQueryString = false; + + // Having `[^\w\d-./]` in there is just a shortcut that lets us skip + // the most common characters in URLs. Replacing that it with `.` would not change + // the result, because encodeURI returns those characters unchanged, but it + // would mean lots of unnecessary replacement calls. Having `[` and `]` in that + // section as well means we do *not* enocde square brackets. These characters are + // a strange beast in URLs, but if anything, this causes URLs to be more readable, + // and we leave it to the browser to make sure that these links are handled without + // problems. + link = link.replace(/%(?:[\da-fA-F]{2})|\?|\+|[^\w\d-./[\]]/g, function(match) { + // Valid percent encoding. Could just return it as is, but we follow RFC3986 + // Section 2.1 which says "For consistency, URI producers and normalizers + // should use uppercase hexadecimal digits for all percent-encodings." + // Note that we also handle (illegal) stand-alone percent characters by + // replacing them with "%25" + if(match.length === 3 && match.charAt(0) == "%") { + return match.toUpperCase(); + } + switch(match) { + case "?": + inQueryString = true; + return "?"; + break; + + // In the query string, a plus and a space are identical -- normalize. + // Not strictly necessary, but identical behavior to the previous version + // of this function. + case "+": + if(inQueryString) + return "%20"; + break; + } + return encodeURI(match); + }) + + if(title) { + title = title.trim ? title.trim() : title.replace(/^\s*/, "").replace(/\s*$/, ""); + title = title.replace(/"/g, "quot;").replace(/\(/g, "(").replace(/\)/g, ")").replace(//g, ">"); + } + return title ? link + ' "' + title + '"' : link; + }); + } + + commandProto.doLinkOrImage = function(chunk, postProcessing, isImage) { + + chunk.trimWhitespace(); + chunk.findTags(/\s*!?\[/, /\][ ]?(?:\n[ ]*)?(\[.*?\])?/); + var background; + + if(chunk.endTag.length > 1 && chunk.startTag.length > 0) { + + chunk.startTag = chunk.startTag.replace(/!?\[/, ""); + chunk.endTag = ""; + this.addLinkDef(chunk, null); + + } else { + + // We're moving start and end tag back into the selection, since (as we're in the else block) we're not + // *removing* a link, but *adding* one, so whatever findTags() found is now back to being part of the + // link text. linkEnteredCallback takes care of escaping any brackets. + chunk.selection = chunk.startTag + chunk.selection + chunk.endTag; + chunk.startTag = chunk.endTag = ""; + + if(/\n\n/.test(chunk.selection)) { + this.addLinkDef(chunk, null); + return; + } + var that = this; + // The function to be executed when you enter a link and press OK or Cancel. + // Marks up the link and adds the ref. + var linkEnteredCallback = function(link) { + + background.parentNode.removeChild(background); + + if(link !== null) { + // ( $1 + // [^\\] anything that's not a backslash + // (?:\\\\)* an even number (this includes zero) of backslashes + // ) + // (?= followed by + // [[\]] an opening or closing bracket + // ) + // + // In other words, a non-escaped bracket. These have to be escaped now to make sure they + // don't count as the end of the link or similar. + // Note that the actual bracket has to be a lookahead, because (in case of to subsequent brackets), + // the bracket in one match may be the "not a backslash" character in the next match, so it + // should not be consumed by the first match. + // The "prepend a space and finally remove it" steps makes sure there is a "not a backslash" at the + // start of the string, so this also works if the selection begins with a bracket. We cannot solve + // this by anchoring with ^, because in the case that the selection starts with two brackets, this + // would mean a zero-width match at the start. Since zero-width matches advance the string position, + // the first bracket could then not act as the "not a backslash" for the second. + chunk.selection = (" " + chunk.selection).replace(/([^\\](?:\\\\)*)(?=[[\]])/g, "$1\\").substr(1); + + var linkDef = " [999]: " + properlyEncoded(link); + + var num = that.addLinkDef(chunk, linkDef); + chunk.startTag = isImage ? "![" : "["; + chunk.endTag = "][" + num + "]"; + + if(!chunk.selection) { + if(isImage) { + chunk.selection = that.getString("imagedescription"); + } else { + chunk.selection = that.getString("linkdescription"); + } + } + } + postProcessing(); + }; + + background = ui.createBackground(); + + if(isImage) { + if(!this.hooks.insertImageDialog(linkEnteredCallback)) + ui.prompt(this.getString("imagedialog"), imageDefaultText, linkEnteredCallback); + } else { + ui.prompt(this.getString("linkdialog"), linkDefaultText, linkEnteredCallback); + } + return true; + } + }; + + // When making a list, hitting shift-enter will put your cursor on the next line + // at the current indent level. + commandProto.doAutoindent = function(chunk, postProcessing) { + + var commandMgr = this, + fakeSelection = false; + + chunk.before = chunk.before.replace(/(\n|^)[ ]{0,3}([*+-]|\d+[.])[ \t]*\n$/, "\n\n"); + chunk.before = chunk.before.replace(/(\n|^)[ ]{0,3}>[ \t]*\n$/, "\n\n"); + chunk.before = chunk.before.replace(/(\n|^)[ \t]+\n$/, "\n\n"); + + // There's no selection, end the cursor wasn't at the end of the line: + // The user wants to split the current list item / code line / blockquote line + // (for the latter it doesn't really matter) in two. Temporarily select the + // (rest of the) line to achieve this. + if(!chunk.selection && !/^[ \t]*(?:\n|$)/.test(chunk.after)) { + chunk.after = chunk.after.replace(/^[^\n]*/, function(wholeMatch) { + chunk.selection = wholeMatch; + return ""; + }); + fakeSelection = true; + } + + if(/(\n|^)[ ]{0,3}([*+-]|\d+[.])[ \t]+.*\n$/.test(chunk.before)) { + if(commandMgr.doList) { + commandMgr.doList(chunk); + } + } + if(/(\n|^)[ ]{0,3}>[ \t]+.*\n$/.test(chunk.before)) { + if(commandMgr.doBlockquote) { + commandMgr.doBlockquote(chunk); + } + } + if(/(\n|^)(\t|[ ]{4,}).*\n$/.test(chunk.before)) { + if(commandMgr.doCode) { + commandMgr.doCode(chunk); + } + } + + if(fakeSelection) { + chunk.after = chunk.selection + chunk.after; + chunk.selection = ""; + } + }; + + commandProto.doBlockquote = function(chunk, postProcessing) { + + chunk.selection = chunk.selection.replace(/^(\n*)([^\r]+?)(\n*)$/, + function(totalMatch, newlinesBefore, text, newlinesAfter) { + chunk.before += newlinesBefore; + chunk.after = newlinesAfter + chunk.after; + return text; + }); + + chunk.before = chunk.before.replace(/(>[ \t]*)$/, + function(totalMatch, blankLine) { + chunk.selection = blankLine + chunk.selection; + return ""; + }); + + chunk.selection = chunk.selection.replace(/^(\s|>)+$/, ""); + chunk.selection = chunk.selection || this.getString("quoteexample"); + + // The original code uses a regular expression to find out how much of the + // text *directly before* the selection already was a blockquote: + + /* + if (chunk.before) { + chunk.before = chunk.before.replace(/\n?$/, "\n"); + } + chunk.before = chunk.before.replace(/(((\n|^)(\n[ \t]*)*>(.+\n)*.*)+(\n[ \t]*)*$)/, + function (totalMatch) { + chunk.startTag = totalMatch; + return ""; + }); + */ + + // This comes down to: + // Go backwards as many lines a possible, such that each line + // a) starts with ">", or + // b) is almost empty, except for whitespace, or + // c) is preceeded by an unbroken chain of non-empty lines + // leading up to a line that starts with ">" and at least one more character + // and in addition + // d) at least one line fulfills a) + // + // Since this is essentially a backwards-moving regex, it's susceptible to + // catstrophic backtracking and can cause the browser to hang; + // see e.g. http://meta.stackexchange.com/questions/9807. + // + // Hence we replaced this by a simple state machine that just goes through the + // lines and checks for a), b), and c). + + var match = "", + leftOver = "", + line; + if(chunk.before) { + var lines = chunk.before.replace(/\n$/, "").split("\n"); + var inChain = false; + for(var i = 0; i < lines.length; i++) { + var good = false; + line = lines[i]; + inChain = inChain && line.length > 0; // c) any non-empty line continues the chain + if(/^>/.test(line)) { // a) + good = true; + if(!inChain && line.length > 1) // c) any line that starts with ">" and has at least one more character starts the chain + inChain = true; + } else if(/^[ \t]*$/.test(line)) { // b) + good = true; + } else { + good = inChain; // c) the line is not empty and does not start with ">", so it matches if and only if we're in the chain + } + if(good) { + match += line + "\n"; + } else { + leftOver += match + line; + match = "\n"; + } + } + if(!/(^|\n)>/.test(match)) { // d) + leftOver += match; + match = ""; + } + } + + chunk.startTag = match; + chunk.before = leftOver; + + // end of change + + if(chunk.after) { + chunk.after = chunk.after.replace(/^\n?/, "\n"); + } + + chunk.after = chunk.after.replace(/^(((\n|^)(\n[ \t]*)*>(.+\n)*.*)+(\n[ \t]*)*)/, + function(totalMatch) { + chunk.endTag = totalMatch; + return ""; + } + ); + + var replaceBlanksInTags = function(useBracket) { + + var replacement = useBracket ? "> " : ""; + + if(chunk.startTag) { + chunk.startTag = chunk.startTag.replace(/\n((>|\s)*)\n$/, + function(totalMatch, markdown) { + return "\n" + markdown.replace(/^[ ]{0,3}>?[ \t]*$/gm, replacement) + "\n"; + }); + } + if(chunk.endTag) { + chunk.endTag = chunk.endTag.replace(/^\n((>|\s)*)\n/, + function(totalMatch, markdown) { + return "\n" + markdown.replace(/^[ ]{0,3}>?[ \t]*$/gm, replacement) + "\n"; + }); + } + }; + + if(/^(?![ ]{0,3}>)/m.test(chunk.selection)) { + this.wrap(chunk, SETTINGS.lineLength - 2); + chunk.selection = chunk.selection.replace(/^/gm, "> "); + replaceBlanksInTags(true); + chunk.skipLines(); + } else { + chunk.selection = chunk.selection.replace(/^[ ]{0,3}> ?/gm, ""); + this.unwrap(chunk); + replaceBlanksInTags(false); + + if(!/^(\n|^)[ ]{0,3}>/.test(chunk.selection) && chunk.startTag) { + chunk.startTag = chunk.startTag.replace(/\n{0,2}$/, "\n\n"); + } + + if(!/(\n|^)[ ]{0,3}>.*$/.test(chunk.selection) && chunk.endTag) { + chunk.endTag = chunk.endTag.replace(/^\n{0,2}/, "\n\n"); + } + } + + chunk.selection = this.hooks.postBlockquoteCreation(chunk.selection); + + if(!/\n/.test(chunk.selection)) { + chunk.selection = chunk.selection.replace(/^(> *)/, + function(wholeMatch, blanks) { + chunk.startTag += blanks; + return ""; + }); + } + }; + + commandProto.doCode = function(chunk, postProcessing) { + + var hasTextBefore = /\S[ ]*$/.test(chunk.before); + var hasTextAfter = /^[ ]*\S/.test(chunk.after); + + // Use 'four space' markdown if the selection is on its own + // line or is multiline. + if((!hasTextAfter && !hasTextBefore) || /\n/.test(chunk.selection)) { + + chunk.before = chunk.before.replace(/[ ]{4}$/, + function(totalMatch) { + chunk.selection = totalMatch + chunk.selection; + return ""; + }); + + var nLinesBack = 1; + var nLinesForward = 1; + + if(/(\n|^)(\t|[ ]{4,}).*\n$/.test(chunk.before)) { + nLinesBack = 0; + } + if(/^\n(\t|[ ]{4,})/.test(chunk.after)) { + nLinesForward = 0; + } + + chunk.skipLines(nLinesBack, nLinesForward); + + if(!chunk.selection) { + chunk.startTag = " "; + chunk.selection = this.getString("codeexample"); + } else { + if(/^[ ]{0,3}\S/m.test(chunk.selection)) { + if(/\n/.test(chunk.selection)) + chunk.selection = chunk.selection.replace(/^/gm, " "); + else // if it's not multiline, do not select the four added spaces; this is more consistent with the doList behavior + chunk.before += " "; + } else { + chunk.selection = chunk.selection.replace(/^(?:[ ]{4}|[ ]{0,3}\t)/gm, ""); + } + } + } else { + // Use backticks (`) to delimit the code block. + + chunk.trimWhitespace(); + chunk.findTags(/`/, /`/); + + if(!chunk.startTag && !chunk.endTag) { + chunk.startTag = chunk.endTag = "`"; + if(!chunk.selection) { + chunk.selection = this.getString("codeexample"); + } + } else if(chunk.endTag && !chunk.startTag) { + chunk.before += chunk.endTag; + chunk.endTag = ""; + } else { + chunk.startTag = chunk.endTag = ""; + } + } + }; + + commandProto.doList = function(chunk, postProcessing, isNumberedList) { + + // These are identical except at the very beginning and end. + // Should probably use the regex extension function to make this clearer. + var previousItemsRegex = /(\n|^)(([ ]{0,3}([*+-]|\d+[.])[ \t]+.*)(\n.+|\n{2,}([*+-].*|\d+[.])[ \t]+.*|\n{2,}[ \t]+\S.*)*)\n*$/; + var nextItemsRegex = /^\n*(([ ]{0,3}([*+-]|\d+[.])[ \t]+.*)(\n.+|\n{2,}([*+-].*|\d+[.])[ \t]+.*|\n{2,}[ \t]+\S.*)*)\n*/; + + // The default bullet is a dash but others are possible. + // This has nothing to do with the particular HTML bullet, + // it's just a markdown bullet. + var bullet = "-"; + + // The number in a numbered list. + var num = 1; + + // Get the item prefix - e.g. " 1. " for a numbered list, " - " for a bulleted list. + var getItemPrefix = function() { + var prefix; + if(isNumberedList) { + prefix = " " + num + ". "; + num++; + } else { + prefix = " " + bullet + " "; + } + return prefix; + }; + + // Fixes the prefixes of the other list items. + var getPrefixedItem = function(itemText) { + + // The numbering flag is unset when called by autoindent. + if(isNumberedList === undefined) { + isNumberedList = /^\s*\d/.test(itemText); + } + + // Renumber/bullet the list element. + itemText = itemText.replace(/^[ ]{0,3}([*+-]|\d+[.])\s/gm, + function(_) { + return getItemPrefix(); + }); + + return itemText; + }; + + chunk.findTags(/(\n|^)*[ ]{0,3}([*+-]|\d+[.])\s+/, null); + + if(chunk.before && !/\n$/.test(chunk.before) && !/^\n/.test(chunk.startTag)) { + chunk.before += chunk.startTag; + chunk.startTag = ""; + } + + if(chunk.startTag) { + + var hasDigits = /\d+[.]/.test(chunk.startTag); + chunk.startTag = ""; + chunk.selection = chunk.selection.replace(/\n[ ]{4}/g, "\n"); + this.unwrap(chunk); + chunk.skipLines(); + + if(hasDigits) { + // Have to renumber the bullet points if this is a numbered list. + chunk.after = chunk.after.replace(nextItemsRegex, getPrefixedItem); + } + if(isNumberedList == hasDigits) { + return; + } + } + + var nLinesUp = 1; + + chunk.before = chunk.before.replace(previousItemsRegex, + function(itemText) { + if(/^\s*([*+-])/.test(itemText)) { + bullet = re.$1; + } + nLinesUp = /[^\n]\n\n[^\n]/.test(itemText) ? 1 : 0; + return getPrefixedItem(itemText); + }); + + if(!chunk.selection) { + chunk.selection = this.getString("litem"); + } + + var prefix = getItemPrefix(); + + var nLinesDown = 1; + + chunk.after = chunk.after.replace(nextItemsRegex, + function(itemText) { + nLinesDown = /[^\n]\n\n[^\n]/.test(itemText) ? 1 : 0; + return getPrefixedItem(itemText); + }); + + chunk.trimWhitespace(true); + chunk.skipLines(nLinesUp, nLinesDown, true); + chunk.startTag = prefix; + var spaces = prefix.replace(/./g, " "); + this.wrap(chunk, SETTINGS.lineLength - spaces.length); + chunk.selection = chunk.selection.replace(/\n/g, "\n" + spaces); + + }; + + commandProto.doHeading = function(chunk, postProcessing) { + + // Remove leading/trailing whitespace and reduce internal spaces to single spaces. + chunk.selection = chunk.selection.replace(/\s+/g, " "); + chunk.selection = chunk.selection.replace(/(^\s+|\s+$)/g, ""); + + // If we clicked the button with no selected text, we just + // make a level 2 hash header around some default text. + if(!chunk.selection) { + chunk.startTag = "## "; + chunk.selection = this.getString("headingexample"); + chunk.endTag = " ##"; + return; + } + + var headerLevel = 0; // The existing header level of the selected text. + + // Remove any existing hash heading markdown and save the header level. + chunk.findTags(/#+[ ]*/, /[ ]*#+/); + if(/#+/.test(chunk.startTag)) { + headerLevel = re.lastMatch.length; + } + chunk.startTag = chunk.endTag = ""; + + // Try to get the current header level by looking for - and = in the line + // below the selection. + chunk.findTags(null, /\s?(-+|=+)/); + if(/=+/.test(chunk.endTag)) { + headerLevel = 1; + } + if(/-+/.test(chunk.endTag)) { + headerLevel = 2; + } + + // Skip to the next line so we can create the header markdown. + chunk.startTag = chunk.endTag = ""; + chunk.skipLines(1, 1); + + // We make a level 2 header if there is no current header. + // If there is a header level, we substract one from the header level. + // If it's already a level 1 header, it's removed. + var headerLevelToCreate = headerLevel == 0 ? 2 : headerLevel - 1; + + if(headerLevelToCreate > 0) { + + // The button only creates level 1 and 2 underline headers. + // Why not have it iterate over hash header levels? Wouldn't that be easier and cleaner? + var headerChar = headerLevelToCreate >= 2 ? "-" : "="; + var len = chunk.selection.length; + if(len > SETTINGS.lineLength) { + len = SETTINGS.lineLength; + } + chunk.endTag = "\n"; + while(len--) { + chunk.endTag += headerChar; + } + } + }; + + commandProto.doHorizontalRule = function(chunk, postProcessing) { + chunk.startTag = "----------\n"; + chunk.selection = ""; + chunk.skipLines(2, 1, true); + } + + + })(); \ No newline at end of file diff --git a/resources/js/pagedown/Markdown.Extra.js b/resources/js/pagedown/Markdown.Extra.js new file mode 100644 index 0000000000..d3b859a542 --- /dev/null +++ b/resources/js/pagedown/Markdown.Extra.js @@ -0,0 +1,874 @@ +(function () { + // A quick way to make sure we're only keeping span-level tags when we need to. + // This isn't supposed to be foolproof. It's just a quick way to make sure we + // keep all span-level tags returned by a pagedown converter. It should allow + // all span-level tags through, with or without attributes. + var inlineTags = new RegExp(['^(<\\/?(a|abbr|acronym|applet|area|b|basefont|', + 'bdo|big|button|cite|code|del|dfn|em|figcaption|', + 'font|i|iframe|img|input|ins|kbd|label|map|', + 'mark|meter|object|param|progress|q|ruby|rp|rt|s|', + 'samp|script|select|small|span|strike|strong|', + 'sub|sup|textarea|time|tt|u|var|wbr)[^>]*>|', + '<(br)\\s?\\/?>)$'].join(''), 'i'); + + /****************************************************************** + * Utility Functions * + *****************************************************************/ + + // patch for ie7 + if (!Array.indexOf) { + Array.prototype.indexOf = function(obj) { + for (var i = 0; i < this.length; i++) { + if (this[i] == obj) { + return i; + } + } + return -1; + }; + } + + function trim(str) { + return str.replace(/^\s+|\s+$/g, ''); + } + + function rtrim(str) { + return str.replace(/\s+$/g, ''); + } + + // Remove one level of indentation from text. Indent is 4 spaces. + function outdent(text) { + return text.replace(new RegExp('^(\\t|[ ]{1,4})', 'gm'), ''); + } + + function contains(str, substr) { + return str.indexOf(substr) != -1; + } + + // Sanitize html, removing tags that aren't in the whitelist + function sanitizeHtml(html, whitelist) { + return html.replace(/<[^>]*>?/gi, function(tag) { + return tag.match(whitelist) ? tag : ''; + }); + } + + // Merge two arrays, keeping only unique elements. + function union(x, y) { + var obj = {}; + for (var i = 0; i < x.length; i++) + obj[x[i]] = x[i]; + for (i = 0; i < y.length; i++) + obj[y[i]] = y[i]; + var res = []; + for (var k in obj) { + if (obj.hasOwnProperty(k)) + res.push(obj[k]); + } + return res; + } + + // JS regexes don't support \A or \Z, so we add sentinels, as Pagedown + // does. In this case, we add the ascii codes for start of text (STX) and + // end of text (ETX), an idea borrowed from: + // https://github.com/tanakahisateru/js-markdown-extra + function addAnchors(text) { + if(text.charAt(0) != '\x02') + text = '\x02' + text; + if(text.charAt(text.length - 1) != '\x03') + text = text + '\x03'; + return text; + } + + // Remove STX and ETX sentinels. + function removeAnchors(text) { + if(text.charAt(0) == '\x02') + text = text.substr(1); + if(text.charAt(text.length - 1) == '\x03') + text = text.substr(0, text.length - 1); + return text; + } + + // Convert markdown within an element, retaining only span-level tags + function convertSpans(text, extra) { + return sanitizeHtml(convertAll(text, extra), inlineTags); + } + + // Convert internal markdown using the stock pagedown converter + function convertAll(text, extra) { + var result = extra.blockGamutHookCallback(text); + // We need to perform these operations since we skip the steps in the converter + result = unescapeSpecialChars(result); + result = result.replace(/~D/g, "$$").replace(/~T/g, "~"); + result = extra.previousPostConversion(result); + return result; + } + + // Convert escaped special characters + function processEscapesStep1(text) { + // Markdown extra adds two escapable characters, `:` and `|` + return text.replace(/\\\|/g, '~I').replace(/\\:/g, '~i'); + } + function processEscapesStep2(text) { + return text.replace(/~I/g, '|').replace(/~i/g, ':'); + } + + // Duplicated from PageDown converter + function unescapeSpecialChars(text) { + // Swap back in all the special characters we've hidden. + text = text.replace(/~E(\d+)E/g, function(wholeMatch, m1) { + var charCodeToReplace = parseInt(m1); + return String.fromCharCode(charCodeToReplace); + }); + return text; + } + + function slugify(text) { + return text.toLowerCase() + .replace(/\s+/g, '-') // Replace spaces with - + .replace(/[^\w\-]+/g, '') // Remove all non-word chars + .replace(/\-\-+/g, '-') // Replace multiple - with single - + .replace(/^-+/, '') // Trim - from start of text + .replace(/-+$/, ''); // Trim - from end of text + } + + /***************************************************************************** + * Markdown.Extra * + ****************************************************************************/ + + Markdown.Extra = function() { + // For converting internal markdown (in tables for instance). + // This is necessary since these methods are meant to be called as + // preConversion hooks, and the Markdown converter passed to init() + // won't convert any markdown contained in the html tags we return. + this.converter = null; + + // Stores html blocks we generate in hooks so that + // they're not destroyed if the user is using a sanitizing converter + this.hashBlocks = []; + + // Stores footnotes + this.footnotes = {}; + this.usedFootnotes = []; + + // Special attribute blocks for fenced code blocks and headers enabled. + this.attributeBlocks = false; + + // Fenced code block options + this.googleCodePrettify = false; + this.highlightJs = false; + + // Table options + this.tableClass = ''; + + this.tabWidth = 4; + }; + + Markdown.Extra.init = function(converter, options) { + // Each call to init creates a new instance of Markdown.Extra so it's + // safe to have multiple converters, with different options, on a single page + var extra = new Markdown.Extra(); + var postNormalizationTransformations = []; + var preBlockGamutTransformations = []; + var postSpanGamutTransformations = []; + var postConversionTransformations = ["unHashExtraBlocks"]; + + options = options || {}; + options.extensions = options.extensions || ["all"]; + if (contains(options.extensions, "all")) { + options.extensions = ["tables", "fenced_code_gfm", "def_list", "attr_list", "footnotes", "smartypants", "strikethrough", "newlines"]; + } + preBlockGamutTransformations.push("wrapHeaders"); + if (contains(options.extensions, "attr_list")) { + postNormalizationTransformations.push("hashFcbAttributeBlocks"); + preBlockGamutTransformations.push("hashHeaderAttributeBlocks"); + postConversionTransformations.push("applyAttributeBlocks"); + extra.attributeBlocks = true; + } + if (contains(options.extensions, "fenced_code_gfm")) { + // This step will convert fcb inside list items and blockquotes + preBlockGamutTransformations.push("fencedCodeBlocks"); + // This extra step is to prevent html blocks hashing and link definition/footnotes stripping inside fcb + postNormalizationTransformations.push("fencedCodeBlocks"); + } + if (contains(options.extensions, "tables")) { + preBlockGamutTransformations.push("tables"); + } + if (contains(options.extensions, "def_list")) { + preBlockGamutTransformations.push("definitionLists"); + } + if (contains(options.extensions, "footnotes")) { + postNormalizationTransformations.push("stripFootnoteDefinitions"); + preBlockGamutTransformations.push("doFootnotes"); + postConversionTransformations.push("printFootnotes"); + } + if (contains(options.extensions, "smartypants")) { + postConversionTransformations.push("runSmartyPants"); + } + if (contains(options.extensions, "strikethrough")) { + postSpanGamutTransformations.push("strikethrough"); + } + if (contains(options.extensions, "newlines")) { + postSpanGamutTransformations.push("newlines"); + } + + converter.hooks.chain("postNormalization", function(text) { + return extra.doTransform(postNormalizationTransformations, text) + '\n'; + }); + + converter.hooks.chain("preBlockGamut", function(text, blockGamutHookCallback) { + // Keep a reference to the block gamut callback to run recursively + extra.blockGamutHookCallback = blockGamutHookCallback; + text = processEscapesStep1(text); + text = extra.doTransform(preBlockGamutTransformations, text) + '\n'; + text = processEscapesStep2(text); + return text; + }); + + converter.hooks.chain("postSpanGamut", function(text) { + return extra.doTransform(postSpanGamutTransformations, text); + }); + + // Keep a reference to the hook chain running before doPostConversion to apply on hashed extra blocks + extra.previousPostConversion = converter.hooks.postConversion; + converter.hooks.chain("postConversion", function(text) { + text = extra.doTransform(postConversionTransformations, text); + // Clear state vars that may use unnecessary memory + extra.hashBlocks = []; + extra.footnotes = {}; + extra.usedFootnotes = []; + return text; + }); + + if ("highlighter" in options) { + extra.googleCodePrettify = options.highlighter === 'prettify'; + extra.highlightJs = options.highlighter === 'highlight'; + } + + if ("table_class" in options) { + extra.tableClass = options.table_class; + } + + extra.converter = converter; + + // Caller usually won't need this, but it's handy for testing. + return extra; + }; + + // Do transformations + Markdown.Extra.prototype.doTransform = function(transformations, text) { + for(var i = 0; i < transformations.length; i++) + text = this[transformations[i]](text); + return text; + }; + + // Return a placeholder containing a key, which is the block's index in the + // hashBlocks array. We wrap our output in a

            tag here so Pagedown won't. + Markdown.Extra.prototype.hashExtraBlock = function(block) { + return '\n

            ~X' + (this.hashBlocks.push(block) - 1) + 'X

            \n'; + }; + Markdown.Extra.prototype.hashExtraInline = function(block) { + return '~X' + (this.hashBlocks.push(block) - 1) + 'X'; + }; + + // Replace placeholder blocks in `text` with their corresponding + // html blocks in the hashBlocks array. + Markdown.Extra.prototype.unHashExtraBlocks = function(text) { + var self = this; + function recursiveUnHash() { + var hasHash = false; + text = text.replace(/(?:

            )?~X(\d+)X(?:<\/p>)?/g, function(wholeMatch, m1) { + hasHash = true; + var key = parseInt(m1, 10); + return self.hashBlocks[key]; + }); + if(hasHash === true) { + recursiveUnHash(); + } + } + recursiveUnHash(); + return text; + }; + + // Wrap headers to make sure they won't be in def lists + Markdown.Extra.prototype.wrapHeaders = function(text) { + function wrap(text) { + return '\n' + text + '\n'; + } + text = text.replace(/^.+[ \t]*\n=+[ \t]*\n+/gm, wrap); + text = text.replace(/^.+[ \t]*\n-+[ \t]*\n+/gm, wrap); + text = text.replace(/^\#{1,6}[ \t]*.+?[ \t]*\#*\n+/gm, wrap); + return text; + }; + + + /****************************************************************** + * Attribute Blocks * + *****************************************************************/ + + // TODO: use sentinels. Should we just add/remove them in doConversion? + // TODO: better matches for id / class attributes + var attrBlock = "\\{[ \\t]*((?:[#.][-_:a-zA-Z0-9]+[ \\t]*)+)\\}"; + var hdrAttributesA = new RegExp("^(#{1,6}.*#{0,6})[ \\t]+" + attrBlock + "[ \\t]*(?:\\n|0x03)", "gm"); + var hdrAttributesB = new RegExp("^(.*)[ \\t]+" + attrBlock + "[ \\t]*\\n" + + "(?=[\\-|=]+\\s*(?:\\n|0x03))", "gm"); // underline lookahead + var fcbAttributes = new RegExp("^(```[^`\\n]*)[ \\t]+" + attrBlock + "[ \\t]*\\n" + + "(?=([\\s\\S]*?)\\n```[ \\t]*(\\n|0x03))", "gm"); + + // Extract headers attribute blocks, move them above the element they will be + // applied to, and hash them for later. + Markdown.Extra.prototype.hashHeaderAttributeBlocks = function(text) { + + var self = this; + function attributeCallback(wholeMatch, pre, attr) { + return '

            ~XX' + (self.hashBlocks.push(attr) - 1) + 'XX

            \n' + pre + "\n"; + } + + text = text.replace(hdrAttributesA, attributeCallback); // ## headers + text = text.replace(hdrAttributesB, attributeCallback); // underline headers + return text; + }; + + // Extract FCB attribute blocks, move them above the element they will be + // applied to, and hash them for later. + Markdown.Extra.prototype.hashFcbAttributeBlocks = function(text) { + // TODO: use sentinels. Should we just add/remove them in doConversion? + // TODO: better matches for id / class attributes + + var self = this; + function attributeCallback(wholeMatch, pre, attr) { + return '

            ~XX' + (self.hashBlocks.push(attr) - 1) + 'XX

            \n' + pre + "\n"; + } + + return text.replace(fcbAttributes, attributeCallback); + }; + + Markdown.Extra.prototype.applyAttributeBlocks = function(text) { + var self = this; + var blockRe = new RegExp('

            ~XX(\\d+)XX

            [\\s]*' + + '(?:<(h[1-6]|pre)(?: +class="(\\S+)")?(>[\\s\\S]*?))', "gm"); + text = text.replace(blockRe, function(wholeMatch, k, tag, cls, rest) { + if (!tag) // no following header or fenced code block. + return ''; + + // get attributes list from hash + var key = parseInt(k, 10); + var attributes = self.hashBlocks[key]; + + // get id + var id = attributes.match(/#[^\s#.]+/g) || []; + var idStr = id[0] ? ' id="' + id[0].substr(1, id[0].length - 1) + '"' : ''; + + // get classes and merge with existing classes + var classes = attributes.match(/\.[^\s#.]+/g) || []; + for (var i = 0; i < classes.length; i++) // Remove leading dot + classes[i] = classes[i].substr(1, classes[i].length - 1); + + var classStr = ''; + if (cls) + classes = union(classes, [cls]); + + if (classes.length > 0) + classStr = ' class="' + classes.join(' ') + '"'; + + return "<" + tag + idStr + classStr + rest; + }); + + return text; + }; + + /****************************************************************** + * Tables * + *****************************************************************/ + + // Find and convert Markdown Extra tables into html. + Markdown.Extra.prototype.tables = function(text) { + var self = this; + + var leadingPipe = new RegExp( + ['^' , + '[ ]{0,3}' , // Allowed whitespace + '[|]' , // Initial pipe + '(.+)\\n' , // $1: Header Row + + '[ ]{0,3}' , // Allowed whitespace + '[|]([ ]*[-:]+[-| :]*)\\n' , // $2: Separator + + '(' , // $3: Table Body + '(?:[ ]*[|].*\\n?)*' , // Table rows + ')', + '(?:\\n|$)' // Stop at final newline + ].join(''), + 'gm' + ); + + var noLeadingPipe = new RegExp( + ['^' , + '[ ]{0,3}' , // Allowed whitespace + '(\\S.*[|].*)\\n' , // $1: Header Row + + '[ ]{0,3}' , // Allowed whitespace + '([-:]+[ ]*[|][-| :]*)\\n' , // $2: Separator + + '(' , // $3: Table Body + '(?:.*[|].*\\n?)*' , // Table rows + ')' , + '(?:\\n|$)' // Stop at final newline + ].join(''), + 'gm' + ); + + text = text.replace(leadingPipe, doTable); + text = text.replace(noLeadingPipe, doTable); + + // $1 = header, $2 = separator, $3 = body + function doTable(match, header, separator, body, offset, string) { + // remove any leading pipes and whitespace + header = header.replace(/^ *[|]/m, ''); + separator = separator.replace(/^ *[|]/m, ''); + body = body.replace(/^ *[|]/gm, ''); + + // remove trailing pipes and whitespace + header = header.replace(/[|] *$/m, ''); + separator = separator.replace(/[|] *$/m, ''); + body = body.replace(/[|] *$/gm, ''); + + // determine column alignments + var alignspecs = separator.split(/ *[|] */); + var align = []; + for (var i = 0; i < alignspecs.length; i++) { + var spec = alignspecs[i]; + if (spec.match(/^ *-+: *$/m)) + align[i] = ' align="right"'; + else if (spec.match(/^ *:-+: *$/m)) + align[i] = ' align="center"'; + else if (spec.match(/^ *:-+ *$/m)) + align[i] = ' align="left"'; + else align[i] = ''; + } + + // TODO: parse spans in header and rows before splitting, so that pipes + // inside of tags are not interpreted as separators + var headers = header.split(/ *[|] */); + var colCount = headers.length; + + // build html + var cls = self.tableClass ? ' class="' + self.tableClass + '"' : ''; + var html = ['\n', '\n', '\n'].join(''); + + // build column headers. + for (i = 0; i < colCount; i++) { + var headerHtml = convertSpans(trim(headers[i]), self); + html += [" ", headerHtml, "\n"].join(''); + } + html += "\n\n"; + + // build rows + var rows = body.split('\n'); + for (i = 0; i < rows.length; i++) { + if (rows[i].match(/^\s*$/)) // can apply to final row + continue; + + // ensure number of rowCells matches colCount + var rowCells = rows[i].split(/ *[|] */); + var lenDiff = colCount - rowCells.length; + for (var j = 0; j < lenDiff; j++) + rowCells.push(''); + + html += "\n"; + for (j = 0; j < colCount; j++) { + var colHtml = convertSpans(trim(rowCells[j]), self); + html += [" ", colHtml, "\n"].join(''); + } + html += "\n"; + } + + html += "\n"; + + // replace html with placeholder until postConversion step + return self.hashExtraBlock(html); + } + + return text; + }; + + + /****************************************************************** + * Footnotes * + *****************************************************************/ + + // Strip footnote, store in hashes. + Markdown.Extra.prototype.stripFootnoteDefinitions = function(text) { + var self = this; + + text = text.replace( + /\n[ ]{0,3}\[\^(.+?)\]\:[ \t]*\n?([\s\S]*?)\n{1,2}((?=\n[ ]{0,3}\S)|$)/g, + function(wholeMatch, m1, m2) { + m1 = slugify(m1); + m2 += "\n"; + m2 = m2.replace(/^[ ]{0,3}/g, ""); + self.footnotes[m1] = m2; + return "\n"; + }); + + return text; + }; + + + // Find and convert footnotes references. + Markdown.Extra.prototype.doFootnotes = function(text) { + var self = this; + if(self.isConvertingFootnote === true) { + return text; + } + + var footnoteCounter = 0; + text = text.replace(/\[\^(.+?)\]/g, function(wholeMatch, m1) { + var id = slugify(m1); + var footnote = self.footnotes[id]; + if (footnote === undefined) { + return wholeMatch; + } + footnoteCounter++; + self.usedFootnotes.push(id); + var html = '' + footnoteCounter + + ''; + return self.hashExtraInline(html); + }); + + return text; + }; + + // Print footnotes at the end of the document + Markdown.Extra.prototype.printFootnotes = function(text) { + var self = this; + + if (self.usedFootnotes.length === 0) { + return text; + } + + text += '\n\n
            \n
            \n
              \n\n'; + for(var i=0; i' + + formattedfootnote + + ' \n\n'; + } + text += '
            \n
            '; + return text; + }; + + + /****************************************************************** + * Fenced Code Blocks (gfm) * + ******************************************************************/ + + // Find and convert gfm-inspired fenced code blocks into html. + Markdown.Extra.prototype.fencedCodeBlocks = function(text) { + function encodeCode(code) { + code = code.replace(/&/g, "&"); + code = code.replace(//g, ">"); + // These were escaped by PageDown before postNormalization + code = code.replace(/~D/g, "$$"); + code = code.replace(/~T/g, "~"); + return code; + } + + var self = this; + text = text.replace(/(?:^|\n)```([^`\n]*)\n([\s\S]*?)\n```[ \t]*(?=\n)/g, function(match, m1, m2) { + var language = trim(m1), codeblock = m2; + + // adhere to specified options + var preclass = self.googleCodePrettify ? ' class="prettyprint"' : ''; + var codeclass = ''; + if (language) { + if (self.googleCodePrettify || self.highlightJs) { + // use html5 language- class names. supported by both prettify and highlight.js + codeclass = ' class="language-' + language + '"'; + } else { + codeclass = ' class="' + language + '"'; + } + } + + var html = ['', + encodeCode(codeblock), '

        '].join(''); + + // replace codeblock with placeholder until postConversion step + return self.hashExtraBlock(html); + }); + + return text; + }; + + + /****************************************************************** + * SmartyPants * + ******************************************************************/ + + Markdown.Extra.prototype.educatePants = function(text) { + var self = this; + var result = ''; + var blockOffset = 0; + // Here we parse HTML in a very bad manner + text.replace(/(?:)|(<)([a-zA-Z1-6]+)([^\n]*?>)([\s\S]*?)(<\/\2>)/g, function(wholeMatch, m1, m2, m3, m4, m5, offset) { + var token = text.substring(blockOffset, offset); + result += self.applyPants(token); + self.smartyPantsLastChar = result.substring(result.length - 1); + blockOffset = offset + wholeMatch.length; + if(!m1) { + // Skip commentary + result += wholeMatch; + return; + } + // Skip special tags + if(!/code|kbd|pre|script|noscript|iframe|math|ins|del|pre/i.test(m2)) { + m4 = self.educatePants(m4); + } + else { + self.smartyPantsLastChar = m4.substring(m4.length - 1); + } + result += m1 + m2 + m3 + m4 + m5; + }); + var lastToken = text.substring(blockOffset); + result += self.applyPants(lastToken); + self.smartyPantsLastChar = result.substring(result.length - 1); + return result; + }; + + function revertPants(wholeMatch, m1) { + var blockText = m1; + blockText = blockText.replace(/&\#8220;/g, "\""); + blockText = blockText.replace(/&\#8221;/g, "\""); + blockText = blockText.replace(/&\#8216;/g, "'"); + blockText = blockText.replace(/&\#8217;/g, "'"); + blockText = blockText.replace(/&\#8212;/g, "---"); + blockText = blockText.replace(/&\#8211;/g, "--"); + blockText = blockText.replace(/&\#8230;/g, "..."); + return blockText; + } + + Markdown.Extra.prototype.applyPants = function(text) { + // Dashes + text = text.replace(/---/g, "—").replace(/--/g, "–"); + // Ellipses + text = text.replace(/\.\.\./g, "…").replace(/\.\s\.\s\./g, "…"); + // Backticks + text = text.replace(/``/g, "“").replace (/''/g, "”"); + + if(/^'$/.test(text)) { + // Special case: single-character ' token + if(/\S/.test(this.smartyPantsLastChar)) { + return "’"; + } + return "‘"; + } + if(/^"$/.test(text)) { + // Special case: single-character " token + if(/\S/.test(this.smartyPantsLastChar)) { + return "”"; + } + return "“"; + } + + // Special case if the very first character is a quote + // followed by punctuation at a non-word-break. Close the quotes by brute force: + text = text.replace (/^'(?=[!"#\$\%'()*+,\-.\/:;<=>?\@\[\\]\^_`{|}~]\B)/, "’"); + text = text.replace (/^"(?=[!"#\$\%'()*+,\-.\/:;<=>?\@\[\\]\^_`{|}~]\B)/, "”"); + + // Special case for double sets of quotes, e.g.: + //

        He said, "'Quoted' words in a larger quote."

        + text = text.replace(/"'(?=\w)/g, "“‘"); + text = text.replace(/'"(?=\w)/g, "‘“"); + + // Special case for decade abbreviations (the '80s): + text = text.replace(/'(?=\d{2}s)/g, "’"); + + // Get most opening single quotes: + text = text.replace(/(\s| |--|&[mn]dash;|&\#8211;|&\#8212;|&\#x201[34];)'(?=\w)/g, "$1‘"); + + // Single closing quotes: + text = text.replace(/([^\s\[\{\(\-])'/g, "$1’"); + text = text.replace(/'(?=\s|s\b)/g, "’"); + + // Any remaining single quotes should be opening ones: + text = text.replace(/'/g, "‘"); + + // Get most opening double quotes: + text = text.replace(/(\s| |--|&[mn]dash;|&\#8211;|&\#8212;|&\#x201[34];)"(?=\w)/g, "$1“"); + + // Double closing quotes: + text = text.replace(/([^\s\[\{\(\-])"/g, "$1”"); + text = text.replace(/"(?=\s)/g, "”"); + + // Any remaining quotes should be opening ones. + text = text.replace(/"/ig, "“"); + return text; + }; + + // Find and convert markdown extra definition lists into html. + Markdown.Extra.prototype.runSmartyPants = function(text) { + this.smartyPantsLastChar = ''; + text = this.educatePants(text); + // Clean everything inside html tags (some of them may have been converted due to our rough html parsing) + text = text.replace(/(<([a-zA-Z1-6]+)\b([^\n>]*?)(\/)?>)/g, revertPants); + return text; + }; + + /****************************************************************** + * Definition Lists * + ******************************************************************/ + + // Find and convert markdown extra definition lists into html. + Markdown.Extra.prototype.definitionLists = function(text) { + var wholeList = new RegExp( + ['(\\x02\\n?|\\n\\n)' , + '(?:' , + '(' , // $1 = whole list + '(' , // $2 + '[ ]{0,3}' , + '((?:[ \\t]*\\S.*\\n)+)', // $3 = defined term + '\\n?' , + '[ ]{0,3}:[ ]+' , // colon starting definition + ')' , + '([\\s\\S]+?)' , + '(' , // $4 + '(?=\\0x03)' , // \z + '|' , + '(?=' , + '\\n{2,}' , + '(?=\\S)' , + '(?!' , // Negative lookahead for another term + '[ ]{0,3}' , + '(?:\\S.*\\n)+?' , // defined term + '\\n?' , + '[ ]{0,3}:[ ]+' , // colon starting definition + ')' , + '(?!' , // Negative lookahead for another definition + '[ ]{0,3}:[ ]+' , // colon starting definition + ')' , + ')' , + ')' , + ')' , + ')' + ].join(''), + 'gm' + ); + + var self = this; + text = addAnchors(text); + + text = text.replace(wholeList, function(match, pre, list) { + var result = trim(self.processDefListItems(list)); + result = "
        \n" + result + "\n
        "; + return pre + self.hashExtraBlock(result) + "\n\n"; + }); + + return removeAnchors(text); + }; + + // Process the contents of a single definition list, splitting it + // into individual term and definition list items. + Markdown.Extra.prototype.processDefListItems = function(listStr) { + var self = this; + + var dt = new RegExp( + ['(\\x02\\n?|\\n\\n+)' , // leading line + '(' , // definition terms = $1 + '[ ]{0,3}' , // leading whitespace + '(?![:][ ]|[ ])' , // negative lookahead for a definition + // mark (colon) or more whitespace + '(?:\\S.*\\n)+?' , // actual term (not whitespace) + ')' , + '(?=\\n?[ ]{0,3}:[ ])' // lookahead for following line feed + ].join(''), // with a definition mark + 'gm' + ); + + var dd = new RegExp( + ['\\n(\\n+)?' , // leading line = $1 + '(' , // marker space = $2 + '[ ]{0,3}' , // whitespace before colon + '[:][ ]+' , // definition mark (colon) + ')' , + '([\\s\\S]+?)' , // definition text = $3 + '(?=\\n*' , // stop at next definition mark, + '(?:' , // next term or end of text + '\\n[ ]{0,3}[:][ ]|' , + '
        |\\x03' , // \z + ')' , + ')' + ].join(''), + 'gm' + ); + + listStr = addAnchors(listStr); + // trim trailing blank lines: + listStr = listStr.replace(/\n{2,}(?=\\x03)/, "\n"); + + // Process definition terms. + listStr = listStr.replace(dt, function(match, pre, termsStr) { + var terms = trim(termsStr).split("\n"); + var text = ''; + for (var i = 0; i < terms.length; i++) { + var term = terms[i]; + // process spans inside dt + term = convertSpans(trim(term), self); + text += "\n
        " + term + "
        "; + } + return text + "\n"; + }); + + // Process actual definitions. + listStr = listStr.replace(dd, function(match, leadingLine, markerSpace, def) { + if (leadingLine || def.match(/\n{2,}/)) { + // replace marker with the appropriate whitespace indentation + def = Array(markerSpace.length + 1).join(' ') + def; + // process markdown inside definition + // TODO?: currently doesn't apply extensions + def = outdent(def) + "\n\n"; + def = "\n" + convertAll(def, self) + "\n"; + } else { + // convert span-level markdown inside definition + def = rtrim(def); + def = convertSpans(outdent(def), self); + } + + return "\n
        " + def + "
        \n"; + }); + + return removeAnchors(listStr); + }; + + + /*********************************************************** + * Strikethrough * + ************************************************************/ + + Markdown.Extra.prototype.strikethrough = function(text) { + // Pretty much duplicated from _DoItalicsAndBold + return text.replace(/([\W_]|^)~T~T(?=\S)([^\r]*?\S[\*_]*)~T~T([\W_]|$)/g, + "$1$2$3"); + }; + + + /*********************************************************** + * New lines * + ************************************************************/ + + Markdown.Extra.prototype.newlines = function(text) { + // We have to ignore already converted newlines and line breaks in sub-list items + return text.replace(/(<(?:br|\/li)>)?\n/g, function(wholeMatch, previousTag) { + return previousTag ? wholeMatch : "
        \n"; + }); + }; + +})(); + diff --git a/themes/HumHub/css/theme.css b/themes/HumHub/css/theme.css deleted file mode 100644 index ee74ea0878..0000000000 --- a/themes/HumHub/css/theme.css +++ /dev/null @@ -1 +0,0 @@ -.colorDefault{color:#ededed}.backgroundDefault{background:#ededed}.borderDefault{border-color:#ededed}.colorPrimary{color:#708fa0 !important}.backgroundPrimary{background:#708fa0 !important}.borderPrimary{border-color:#708fa0 !important}.colorInfo{color:#6fdbe8 !important}.backgroundInfo{background:#6fdbe8 !important}.borderInfo{border-color:#6fdbe8 !important}.colorSuccess{color:#97d271 !important}.backgroundSuccess{background:#97d271 !important}.borderSuccess{border-color:#97d271 !important}.colorWarning{color:#fdd198 !important}.backgroundWarning{background:#fdd198 !important}.borderWarning{border-color:#fdd198 !important}.colorDanger{color:#ff8989 !important}.backgroundDanger{background:#ff8989 !important}.borderDanger{border-color:#ff8989 !important}.colorFont1{color:#bac2c7 !important}.colorFont2{color:#7a7a7a !important}.colorFont3{color:#555 !important}.colorFont4{color:#bebebe !important}.colorFont5{color:#aeaeae !important}body{padding-top:130px;background-color:#ededed;color:#777;font-family:'Open Sans',sans-serif}body a,body a:hover,body a:focus,body a:active,body a.active{color:#555;text-decoration:none}a:hover{text-decoration:none}hr{margin-top:10px;margin-bottom:10px}.col-md-1,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9,.col-md-10,.col-md-11,.col-md-12{position:inherit}textarea{height:1.5em}h4{font-weight:300;font-size:150%}.heading{font-size:16px;font-weight:300;color:#555;background-color:white;border:none;padding:10px}.text-center{text-align:center !important}input[type=text],input[type=password],input[type=select]{-webkit-appearance:none;-moz-appearance:none;appearance:none}.text-break{overflow-wrap:break-word;word-wrap:break-word;-ms-word-break:break-all;word-break:break-word;-ms-hyphens:auto;-moz-hyphens:auto;-webkit-hyphens:auto;hyphens:auto}.login-container{background-color:#708fa0;background-image:linear-gradient(to right, #708fa0 0, #8fa7b4 50%, #8fa7b4 100%),linear-gradient(to right, #7f9baa 0, #bdcbd3 51%, #adbfc9 100%);background-size:100% 100%;position:relative;padding-top:40px}.login-container .text{color:#fff;font-size:12px;margin-bottom:15px}.login-container .text a{color:#fff;text-decoration:underline}.login-container .panel a{color:#6fdbe8}.login-container h1,.login-container h2{color:#fff !important}.login-container .panel{box-shadow:0 0 15px #627d92;-moz-box-shadow:0 0 15px #627d92;-webkit-box-shadow:0 0 15px #627d92}.login-container .panel .panel-heading,.login-container .panel .panel-body{padding:15px}.login-container select{color:#555}#account-login-form .form-group{margin-bottom:10px}.topbar{position:fixed;display:block;height:50px;width:100%;padding-left:15px;padding-right:15px}.topbar ul.nav{float:left}.topbar ul.nav>li{float:left}.topbar ul.nav>li>a{padding-top:15px;padding-bottom:15px;line-height:20px}.topbar .dropdown-footer{margin:10px}.topbar .dropdown-header{font-size:16px;padding:3px 10px;margin-bottom:10px;font-weight:300;color:#bebebe}.topbar .dropdown-header .dropdown-header-link{position:absolute;top:2px;right:10px}.topbar .dropdown-header .dropdown-header-link a{color:#6fdbe8 !important;font-size:12px;font-weight:normal}.topbar .dropdown-header:hover{color:#bebebe}#topbar-first{background-color:#708fa0;top:0;z-index:1030;color:white}#topbar-first .nav>li>a:hover,#topbar-first .nav>.open>a{background-color:#8fa7b4}#topbar-first .nav>.account{height:50px;margin-left:20px}#topbar-first .nav>.account img{margin-left:10px}#topbar-first .nav>.account .dropdown-toggle{padding:10px 5px 8px;line-height:1.1em;text-align:left}#topbar-first .nav>.account .dropdown-toggle span{font-size:12px}#topbar-first .topbar-brand{position:relative;z-index:2}#topbar-first .topbar-actions{position:relative;z-index:3}#topbar-first .notifications{position:absolute;left:0;right:0;text-align:center;z-index:1}#topbar-first .notifications .btn-group{position:relative;text-align:left}#topbar-first .notifications .btn-group>a{padding:5px 10px;margin:10px 2px;display:inline-block;border-radius:2px;text-decoration:none;text-align:left}#topbar-first .notifications .btn-group>.label{position:absolute;top:4px;right:-2px}#topbar-first .notifications .arrow:after{position:absolute;display:block;width:0;height:0;border-color:transparent;border-style:solid;border-width:10px;content:" ";top:1px;margin-left:-10px;border-top-width:0;border-bottom-color:#fff;z-index:1035}#topbar-first .notifications .arrow{position:absolute;display:block;width:0;height:0;border-color:transparent;border-style:solid;z-index:1001;border-width:11px;left:50%;margin-left:-18px;border-top-width:0;border-bottom-color:rgba(0,0,0,0.15);top:-19px;z-index:1035}#topbar-first .notifications .dropdown-menu{width:350px;margin-left:-148px}#topbar-first .notifications .dropdown-menu ul.media-list{max-height:400px;overflow:auto}#topbar-first .notifications .dropdown-menu li{position:relative}#topbar-first .notifications .dropdown-menu li i.approval{position:absolute;left:2px;top:36px;font-size:14px}#topbar-first .notifications .dropdown-menu li i.accepted{color:#5cb85c}#topbar-first .notifications .dropdown-menu li i.declined{color:#d9534f}#topbar-first .notifications .dropdown-menu li .media{position:relative}#topbar-first .notifications .dropdown-menu li .media .img-space{position:absolute;top:14px;left:14px}#topbar-first .dropdown-footer{margin:10px 10px 5px}#topbar-first a{color:white}#topbar-first .caret{border-top-color:#bebebe}#topbar-first .btn-group>a{background-color:#7f9baa}#topbar-first .btn-enter{background-color:#7f9baa;margin:6px 0}#topbar-first .btn-enter:hover{background-color:#89a2b0}#topbar-first .media-list a{color:#555;padding:0}#topbar-first .media-list li{color:#555}#topbar-first .media-list li i.accepted{color:#6fdbe8 !important}#topbar-first .media-list li i.declined{color:#ff8989 !important}#topbar-first .media-list li.placeholder{border-bottom:none}#topbar-first .media-list .media .media-body .label{padding:.1em .5em}#topbar-first .account .user-title{text-align:right}#topbar-first .account .user-title span{color:#d7d7d7}#topbar-first .dropdown.account>a,#topbar-first .dropdown.account.open>a,#topbar-first .dropdown.account>a:hover,#topbar-first .dropdown.account.open>a:hover{background-color:#708fa0}#topbar-second{top:50px;background-color:#fff;z-index:1029;background-image:none;-webkit-box-shadow:0 1px 10px rgba(0,0,0,0.1);-moz-box-shadow:0 1px 10px rgba(0,0,0,0.1);box-shadow:0 1px 10px rgba(0,0,0,0.1);border-bottom:1px solid #d4d4d4}#topbar-second .dropdown-menu{padding-top:0;padding-bottom:0}#topbar-second .dropdown-menu .divider{margin:0}#topbar-second #space-menu-dropdown,#topbar-second #search-menu-dropdown{width:400px}#topbar-second #space-menu-dropdown .media-list,#topbar-second #search-menu-dropdown .media-list{max-height:400px;overflow:auto}@media screen and (max-width:768px){#topbar-second #space-menu-dropdown .media-list,#topbar-second #search-menu-dropdown .media-list{max-height:200px}}#topbar-second #space-menu-dropdown form,#topbar-second #search-menu-dropdown form{margin:10px}#topbar-second #space-menu-dropdown .search-reset,#topbar-second #search-menu-dropdown .search-reset{position:absolute;color:#BFBFBF;margin:7px;top:0;right:40px;z-index:10;display:none;cursor:pointer}#topbar-second .nav>li>a{padding:6px 13px 0;text-decoration:none;text-shadow:none;font-weight:600;font-size:10px;text-transform:uppercase;text-align:center;min-height:49px}#topbar-second .nav>li>a:hover,#topbar-second .nav>li>a:active,#topbar-second .nav>li>a:focus{border-bottom:3px solid #6fdbe8;background-color:#f7f7f7;color:#555;text-decoration:none}#topbar-second .nav>li>a i{font-size:14px}#topbar-second .nav>li>a .caret{border-top-color:#7a7a7a}#topbar-second .nav>li>ul>li>a{border-left:3px solid #fff;background-color:#fff;color:#555}#topbar-second .nav>li>ul>li>a:hover,#topbar-second .nav>li>ul>li>a.active{border-left:3px solid #6fdbe8;background-color:#f7f7f7;color:#555}#topbar-second .nav>li.active>a{min-height:46px}#topbar-second .nav>li>a#space-menu{padding-right:13px;border-right:1px solid #ededed}#topbar-second .nav>li>a#search-menu{padding-top:15px}#topbar-second .nav>li>a:hover,#topbar-second .nav .open>a,#topbar-second .nav>li.active{border-bottom:3px solid #6fdbe8;background-color:#f7f7f7;color:#555}#topbar-second .nav>li.active>a:hover{border-bottom:none}#topbar-second #space-menu-dropdown li>ul>li>a>.media .media-body p{color:#bebebe;font-size:11px;margin:0;font-weight:400}.dropdown-menu li a{font-size:13px !important;font-weight:600 !important}.dropdown-menu li a i{margin-right:5px;font-size:14px;display:inline-block;width:14px}.dropdown-menu li a:hover,.dropdown-menu li a:visited,.dropdown-menu li a:hover,.dropdown-menu li a:focus{background:none;cursor:pointer}.dropdown-menu li:hover,.dropdown-menu li.selected{color:#555}.dropdown-menu li:first-child{margin-top:3px}.dropdown-menu li:last-child{margin-bottom:3px}.media-list li{padding:10px;border-bottom:1px solid #eee;position:relative;border-left:3px solid white;font-size:12px}.media-list li a{color:#555}.media-list .badge-space-type{background-color:#f7f7f7;border:1px solid #d7d7d7;color:#b2b2b2;padding:3px 3px 2px 3px}.media-list li.new{border-left:3px solid #f3fcfd;background-color:#f3fcfd}.media-list li:hover,.media-list li.selected{background-color:#f7f7f7;border-left:3px solid #6fdbe8}.media-left,.media>.pull-left{padding-right:0;margin-right:10px}.media:after{content:'';clear:both;display:block}.media .time{font-size:11px;color:#bebebe}.media .img-space{position:absolute;top:35px;left:35px}.media .media-body{font-size:13px}.media .media-body h4.media-heading{font-size:14px;font-weight:500;color:#555}.media .media-body h4.media-heading a{color:#555}.media .media-body h4.media-heading small,.media .media-body h4.media-heading small a{font-size:11px;color:#bebebe}.media .media-body h4.media-heading .content{margin-right:35px}.media .media-body .content a{word-break:break-all}.media .media-body h5{color:#aeaeae;font-weight:300;margin-top:5px;margin-bottom:5px;min-height:15px}.media .media-body .module-controls{font-size:85%}.media .media-body .module-controls a{color:#6fdbe8}.media .content a{color:#6fdbe8}.media .content .files a{color:#555}.content span{overflow-wrap:break-word;word-wrap:break-word;-ms-word-break:break-all;word-break:break-word;-ms-hyphens:auto;-moz-hyphens:auto;-webkit-hyphens:auto;hyphens:auto}.module-installed{opacity:.5}.module-installed .label-success{background-color:#d7d7d7}.modal .dropdown-menu,.panel .dropdown-menu,.nav-tabs .dropdown-menu{border:1px solid #d7d7d7}.modal .dropdown-menu li.divider,.panel .dropdown-menu li.divider,.nav-tabs .dropdown-menu li.divider{background-color:#f7f7f7;border-bottom:none;margin:9px 1px !important}.modal .dropdown-menu li,.panel .dropdown-menu li,.nav-tabs .dropdown-menu li{border-left:3px solid white}.modal .dropdown-menu li a,.panel .dropdown-menu li a,.nav-tabs .dropdown-menu li a{color:#555;font-size:14px;font-weight:400;padding:4px 15px}.modal .dropdown-menu li a i,.panel .dropdown-menu li a i,.nav-tabs .dropdown-menu li a i{margin-right:5px}.modal .dropdown-menu li a:hover,.panel .dropdown-menu li a:hover,.nav-tabs .dropdown-menu li a:hover{background:none}.modal .dropdown-menu li:hover,.panel .dropdown-menu li:hover,.nav-tabs .dropdown-menu li:hover,.modal .dropdown-menu li.selected,.panel .dropdown-menu li.selected,.nav-tabs .dropdown-menu li.selected{border-left:3px solid #6fdbe8;background-color:#f7f7f7 !important}.panel{border:none;background-color:#fff;box-shadow:0 0 3px #dadada;-webkit-box-shadow:0 0 3px #dadada;-moz-box-shadow:0 0 3px #dadada;border-radius:4px;position:relative}.panel h1{font-size:16px;font-weight:300;margin-top:0;color:#555}.panel .panel-heading{font-size:16px;font-weight:300;color:#555;background-color:white;border:none;padding:10px;border-radius:4px}.panel .panel-heading .heading-link{color:#6fdbe8 !important;font-size:.8em}.panel .panel-body{padding:10px;font-size:13px}.panel .panel-body p{color:#555}.panel .statistics .entry{margin-left:20px;font-size:12px}.panel .statistics .entry .count{color:#6fdbe8;font-weight:600;font-size:20px;line-height:.8em}.panel h3.media-heading small{font-size:75%}.panel h3.media-heading small a{color:#6fdbe8}.panel-danger{border:2px solid #ff8989}.panel-danger .panel-heading{color:#ff8989}.panel-success{border:2px solid #97d271}.panel-success .panel-heading{color:#97d271}.panel-warning{border:2px solid #fdd198}.panel-warning .panel-heading{color:#fdd198}.panel.profile{position:relative}.panel.profile .controls{position:absolute;top:10px;right:10px}.panel.members .panel-body a img,.panel.groups .panel-body a img,.panel.follower .panel-body a img,.panel.spaces .panel-body a img{margin-bottom:5px}.panel-profile .panel-profile-header{position:relative;border:3px solid #fff;border-top-right-radius:3px;border-top-left-radius:3px}.panel-profile .panel-profile-header .img-profile-header-background{border-radius:3px;min-height:110px}.panel-profile .panel-profile-header .img-profile-data{position:absolute;height:100px;width:100%;bottom:0;left:0;padding-left:180px;padding-top:30px;border-bottom-right-radius:3px;border-bottom-left-radius:3px;color:#fff;pointer-events:none;background:-moz-linear-gradient(top, rgba(0,0,0,0) 0, rgba(0,0,0,0) 1%, rgba(0,0,0,0.38) 100%);background:-webkit-gradient(linear, left top, left bottom, color-stop(0, rgba(0,0,0,0)), color-stop(1%, rgba(0,0,0,0)), color-stop(100%, rgba(0,0,0,0.38)));background:-webkit-linear-gradient(top, rgba(0,0,0,0) 0, rgba(0,0,0,0) 1%, rgba(0,0,0,0.38) 100%);background:-o-linear-gradient(top, rgba(0,0,0,0) 0, rgba(0,0,0,0) 1%, rgba(0,0,0,0.38) 100%);background:-ms-linear-gradient(top, rgba(0,0,0,0) 0, rgba(0,0,0,0) 1%, rgba(0,0,0,0.38) 100%);background:linear-gradient(to bottom, rgba(0,0,0,0) 0, rgba(0,0,0,0) 1%, rgba(0,0,0,0.38) 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#00000000', endColorstr='#94000000', GradientType=0)}.panel-profile .panel-profile-header .img-profile-data h1{font-size:30px;font-weight:100;margin-bottom:7px;color:#fff;max-width:600px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.panel-profile .panel-profile-header .img-profile-data h2{font-size:16px;font-weight:400;margin-top:0}.panel-profile .panel-profile-header .img-profile-data h1.space{font-size:30px;font-weight:700}.panel-profile .panel-profile-header .img-profile-data h2.space{font-size:13px;font-weight:300;max-width:600px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.panel-profile .panel-profile-header .profile-user-photo-container{position:absolute;bottom:-50px;left:15px}.panel-profile .panel-profile-header .profile-user-photo-container .profile-user-photo{border:3px solid #fff;border-radius:5px}.panel-profile .panel-profile-controls{padding-left:160px}.installer .logo{text-align:center}.installer h2{font-weight:100}.installer .panel{margin-top:50px}.installer .panel h3{margin-top:0}.installer .powered,.installer .powered a{color:#bac2c7 !important;margin-top:10px;font-size:12px}.installer .fa{width:18px}.installer .check-ok{color:#97d271}.installer .check-warning{color:#fdd198}.installer .check-error{color:#ff8989}.installer .prerequisites-list ul{list-style:none;padding-left:15px}.installer .prerequisites-list ul li{padding-bottom:5px}.space-acronym{color:#fff;text-align:center;display:inline-block}.current-space-image{margin-right:3px;margin-top:3px}.pagination-container{text-align:center}.pagination>.active>a,.pagination>.active>span,.pagination>.active>a:hover,.pagination>.active>span:hover,.pagination>.active>a:focus,.pagination>.active>span:focus{background-color:#708fa0;border-color:#708fa0}.pagination>li>a,.pagination>li>span,.pagination>li>a:hover,.pagination>li>a:active,.pagination>li>a:focus{color:#555}.well-small{padding:10px;border-radius:3px}.well{border:none;box-shadow:none;background-color:#ededed;margin-bottom:1px}.well hr{margin:15px 0 10px;border-top:1px solid #d9d9d9}.well table>thead{font-size:11px}.img-rounded{-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px}.tab-sub-menu{padding-left:10px}.tab-sub-menu li>a:hover,.tab-sub-menu li>a:focus{background-color:#f7f7f7;border-bottom-color:#ddd}.tab-sub-menu li.active>a{background-color:#FFFFFF;border-bottom-color:transparent}.tab-menu{padding-top:10px;background-color:#FFFFFF}.tab-menu .nav-tabs{padding-left:10px}.tab-menu .nav-tabs li>a{padding-top:12px;border-color:#ddd;border-bottom:1px solid #ddd;background-color:#f7f7f7;max-height:41px;outline:none}.tab-menu .nav-tabs li>a:hover,.tab-menu .nav-tabs li>a:focus{padding-top:10px;border-top:3px solid #ddd}.tab-menu .nav-tabs li>a:hover{background-color:#f7f7f7}.tab-menu .nav-tabs li.active>a,.tab-menu .nav-tabs li.active>a:hover{padding-top:10px;border-top:3px solid #6fdbe8}.tab-menu .nav-tabs li.active>a{background-color:#FFFFFF;border-bottom-color:transparent}.nav-pills .dropdown-menu,.nav-tabs .dropdown-menu,.account .dropdown-menu{background-color:#708fa0;border:none}.nav-pills .dropdown-menu li.divider,.nav-tabs .dropdown-menu li.divider,.account .dropdown-menu li.divider{background-color:#628394;border-bottom:none;margin:9px 1px !important}.nav-pills .dropdown-menu li,.nav-tabs .dropdown-menu li,.account .dropdown-menu li{border-left:3px solid #708fa0}.nav-pills .dropdown-menu li a,.nav-tabs .dropdown-menu li a,.account .dropdown-menu li a{color:white;font-weight:400;font-size:13px;padding:4px 15px}.nav-pills .dropdown-menu li a i,.nav-tabs .dropdown-menu li a i,.account .dropdown-menu li a i{margin-right:5px;font-size:14px;display:inline-block;width:14px}.nav-pills .dropdown-menu li a:hover,.nav-tabs .dropdown-menu li a:hover,.account .dropdown-menu li a:hover,.nav-pills .dropdown-menu li a:visited,.nav-tabs .dropdown-menu li a:visited,.account .dropdown-menu li a:visited,.nav-pills .dropdown-menu li a:hover,.nav-tabs .dropdown-menu li a:hover,.account .dropdown-menu li a:hover,.nav-pills .dropdown-menu li a:focus,.nav-tabs .dropdown-menu li a:focus,.account .dropdown-menu li a:focus{background:none}.nav-pills .dropdown-menu li:hover,.nav-tabs .dropdown-menu li:hover,.account .dropdown-menu li:hover,.nav-pills .dropdown-menu li.selected,.nav-tabs .dropdown-menu li.selected,.account .dropdown-menu li.selected{border-left:3px solid #6fdbe8;color:#fff !important;background-color:#628394 !important}.nav-pills.preferences .dropdown .dropdown-toggle{color:#bebebe}.nav-pills.preferences .dropdown.open .dropdown-toggle,.nav-pills.preferences .dropdown.open .dropdown-toggle:hover{background-color:#708fa0}.popover{border:1px solid rgba(0,0,0,0.15);border-radius:4px;-webkit-box-shadow:0 6px 12px rgba(0,0,0,0.175);-moz-box-shadow:0 6px 12px rgba(0,0,0,0.175);box-shadow:0 6px 12px rgba(0,0,0,0.175)}.popover .popover-title{background:none;border-bottom:none;color:#555;font-weight:300;font-size:16px;padding:15px}.popover .popover-content{font-size:13px;padding:5px 15px;color:#555}.popover .popover-content a{color:#6fdbe8}.popover .popover-navigation{padding:15px}.nav-pills>li.active>a,.nav-pills>li.active>a:hover,.nav-pills>li.active>a:focus{background-color:#708fa0}.nav-tabs{margin-bottom:10px}.list-group a [class^="fa-"],.list-group a [class*=" fa-"]{display:inline-block;width:18px}.nav-pills.preferences{position:absolute;right:10px;top:10px}.nav-pills.preferences .dropdown .dropdown-toggle{padding:2px 10px}.nav-pills.preferences .dropdown.open .dropdown-toggle,.nav-pills.preferences .dropdown.open .dropdown-toggle:hover{color:white}.nav-tabs li{font-weight:600;font-size:12px}.tab-content .tab-pane a{color:#6fdbe8}.tab-content .tab-pane .form-group{margin-bottom:5px}.nav-tabs.tabs-center li{float:none;display:inline-block}.nav-tabs.tabs-small li>a{padding:5px 7px}.nav .caret,.nav .caret:hover,.nav .caret:active{border-top-color:#555;border-bottom-color:#555}.nav li.dropdown>a:hover .caret,.nav li.dropdown>a:active .caret{border-top-color:#555;border-bottom-color:#555}.nav .open>a .caret,.nav .open>a:hover .caret,.nav .open>a:focus .caret{border-top-color:#555;border-bottom-color:#555}.nav .open>a,.nav .open>a:hover,.nav .open>a:focus{border-color:#ededed;color:#555}.nav .open>a .caret,.nav .open>a:hover .caret,.nav .open>a:focus .caret{color:#555}.btn{float:none;border:none;-webkit-box-shadow:none;box-shadow:none;-moz-box-shadow:none;background-image:none;text-shadow:none;border-radius:3px;outline:none !important;margin-bottom:0;font-size:14px;font-weight:600;padding:8px 16px}.input.btn{outline:none}.btn-lg{padding:16px 28px}.btn-sm{padding:4px 8px;font-size:12px}.btn-sm i{font-size:14px}.btn-xs{padding:1px 5px;font-size:12px}.btn-default{background:#ededed;color:#7a7a7a !important}.btn-default:hover,.btn-default:focus{background:#e8e8e8;text-decoration:none;color:#7a7a7a}.btn-default:active,.btn-default.active{outline:0;background:#e0e0e0}.btn-default[disabled],.btn-default.disabled{background:#f2f2f2}.btn-default[disabled]:hover,.btn-default.disabled:hover,.btn-default[disabled]:focus,.btn-default.disabled:focus{background:#f2f2f2}.btn-default[disabled]:active,.btn-default.disabled:active,.btn-default[disabled].active,.btn-default.disabled.active{background:#f2f2f2}.btn-primary{background:#708fa0;color:white !important}.btn-primary:hover,.btn-primary:focus{background:#628394;text-decoration:none}.btn-primary:active,.btn-primary.active{outline:0;background:#628394 !important}.btn-primary[disabled],.btn-primary.disabled{background:#7f9baa}.btn-primary[disabled]:hover,.btn-primary.disabled:hover,.btn-primary[disabled]:focus,.btn-primary.disabled:focus{background:#7f9baa}.btn-primary[disabled]:active,.btn-primary.disabled:active,.btn-primary[disabled].active,.btn-primary.disabled.active{background:#7f9baa !important}.btn-info{background:#6fdbe8;color:white !important}.btn-info:hover,.btn-info:focus{background:#59d6e4 !important;text-decoration:none}.btn-info:active,.btn-info.active{outline:0;background:#59d6e4}.btn-info[disabled],.btn-info.disabled{background:#85e0ec}.btn-info[disabled]:hover,.btn-info.disabled:hover,.btn-info[disabled]:focus,.btn-info.disabled:focus{background:#85e0ec}.btn-info[disabled]:active,.btn-info.disabled:active,.btn-info[disabled].active,.btn-info.disabled.active{background:#85e0ec !important}.btn-danger{background:#ff8989;color:white !important}.btn-danger:hover,.btn-danger:focus{background:#ff6f6f;text-decoration:none}.btn-danger:active,.btn-danger.active{outline:0;background:#ff6f6f !important}.btn-danger[disabled],.btn-danger.disabled{background:#ffa3a3}.btn-danger[disabled]:hover,.btn-danger.disabled:hover,.btn-danger[disabled]:focus,.btn-danger.disabled:focus{background:#ffa3a3}.btn-danger[disabled]:active,.btn-danger.disabled:active,.btn-danger[disabled].active,.btn-danger.disabled.active{background:#ffa3a3 !important}.btn-success{background:#97d271;color:white !important}.btn-success:hover,.btn-success:focus{background:#89cc5e;text-decoration:none}.btn-success:active,.btn-success.active{outline:0;background:#89cc5e !important}.btn-success[disabled],.btn-success.disabled{background:#a5d884}.btn-success[disabled]:hover,.btn-success.disabled:hover,.btn-success[disabled]:focus,.btn-success.disabled:focus{background:#a5d884}.btn-success[disabled]:active,.btn-success.disabled:active,.btn-success[disabled].active,.btn-success.disabled.active{background:#a5d884 !important}.btn-warning{background:#fdd198;color:white !important}.btn-warning:hover,.btn-warning:focus{background:#fdcd8e;text-decoration:none}.btn-warning:active,.btn-warning.active{outline:0;background:#fdcd8e !important}.btn-warning[disabled],.btn-warning.disabled{background:#fddcb1}.btn-warning[disabled]:hover,.btn-warning.disabled:hover,.btn-warning[disabled]:focus,.btn-warning.disabled:focus{background:#fddcb1}.btn-warning[disabled]:active,.btn-warning.disabled:active,.btn-warning[disabled].active,.btn-warning.disabled.active{background:#fddcb1 !important}.radio,.checkbox{margin-top:5px !important;margin-bottom:0}.radio label,.checkbox label{padding-left:10px}.form-control{border:2px solid #ededed;box-shadow:none}.form-control:focus{border:2px solid #6fdbe8;outline:0;box-shadow:none}.form-control.form-search{border-radius:30px;background-image:url("../img/icon_search16x16.png");background-repeat:no-repeat;background-position:10px 8px;padding-left:34px}.form-group-search{position:relative}.form-group-search .form-button-search{position:absolute;top:4px;right:4px;border-radius:30px}textarea{resize:none}select.form-control{-webkit-appearance:none;-moz-appearance:none;appearance:none;background-image:url("../img/select_arrow.png") !important;background-repeat:no-repeat;background-position:right 13px;overflow:hidden}label{font-weight:normal}label.control-label{font-weight:bold}::-webkit-input-placeholder{color:#bebebe !important}::-moz-placeholder{color:#bebebe !important}:-ms-input-placeholder{color:#bebebe !important}input:-moz-placeholder{color:#bebebe !important}.help-block-error{font-size:12px}.help-block:not(.help-block-error){color:#aeaeae !important;font-size:12px}.input-group-addon{border:none}.label{text-transform:uppercase}.label{text-transform:uppercase;display:inline-block;padding:3px 5px 4px;font-weight:600;font-size:10px !important;color:white !important;vertical-align:baseline;white-space:nowrap;text-shadow:none}.label-default{background:#ededed;color:#7a7a7a !important}a.label-default:hover{background:#e0e0e0 !important}.label-info{background-color:#6fdbe8}a.label-info:hover{background:#59d6e4 !important}.label-danger{background-color:#ff8989}a.label-danger:hover{background:#ff6f6f !important}.label-success{background-color:#6fdbe8}a.label-success:hover{background:#59d6e4 !important}.label-warning{background-color:#fdd198}a.label-warning:hover{background:#fdc67f !important}.alert-default{color:#555;background-color:#f7f7f7;border-color:#ededed;font-size:13px}.alert-default .info{margin:10px 0}.alert-success{color:#84be5e;background-color:#f7fbf4;border-color:#97d271}.alert-warning{color:#e9b168;background-color:#fffbf7;border-color:#fdd198}.alert-danger{color:#ff8989;background-color:#fff6f6;border-color:#ff8989}.badge-space{margin-top:6px}.badge-space-chooser{padding:3px 5px;margin-left:1px}.badge{padding:3px 5px;border-radius:2px;font-weight:normal;font-family:Arial,sans-serif;font-size:10px !important;text-transform:uppercase;color:#fff;vertical-align:baseline;white-space:nowrap;text-shadow:none;background-color:#d7d7d7;line-height:1}.list-group-item{padding:6px 15px;border:none;border-width:0 !important;border-left:3px solid #fff !important;font-size:12px;font-weight:600}.list-group-item i{font-size:14px}a.list-group-item:hover,a.list-group-item.active,a.list-group-item.active:hover,a.list-group-item.active:focus{z-index:2;color:#555;background-color:#f7f7f7;border-left:3px solid #6fdbe8 !important}@media screen and (max-width:768px){.modal-dialog{width:auto !important;padding-top:30px;padding-bottom:30px}}.modal-top{z-index:999999 !important}.modal-open{overflow:visible}.modal{overflow-y:visible}.modal-dialog-extra-small{width:400px}.modal-dialog-small{width:500px}.modal-dialog-normal{width:600px}.modal-dialog-medium{width:768px}.modal-dialog-large{width:900px}@media screen and (max-width:920px){.modal-dialog-large{width:auto !important;padding-top:30px;padding-bottom:30px}}.modal{border:none}.modal h1,.modal h2,.modal h3,.modal h4,.modal h5{margin-top:20px;color:#555;font-weight:300}.modal h4.media-heading{margin-top:0}.modal-title{font-size:20px;font-weight:200;color:#555}.modal-dialog,.modal-content{min-width:150px}.modal-content{-webkit-border-radius:3px;-moz-border-radius:3px;box-shadow:0 2px 26px rgba(0,0,0,0.3),0 0 0 1px rgba(0,0,0,0.1);-webkit-box-shadow:0 2px 26px rgba(0,0,0,0.3),0 0 0 1px rgba(0,0,0,0.1);-moz-box-shadow:0 2px 26px rgba(0,0,0,0.3),0 0 0 1px rgba(0,0,0,0.1);border:none}.modal-content .modal-header{padding:20px 20px 0;border-bottom:none;text-align:center}.modal-content .modal-header .close{margin-top:2px;margin-right:5px}.modal-content .modal-body{padding:20px;font-size:13px}.modal-content .modal-footer{margin-top:0;text-align:left;padding:10px 20px 30px;border-top:none;text-align:center}.modal-content .modal-footer hr{margin-top:0}.modal-backdrop{background-color:rgba(0,0,0,0.5)}.progress{height:10px;margin-bottom:15px;box-shadow:none;background:#ededed;border-radius:10px}.progress-bar-info{background-color:#6fdbe8;-webkit-box-shadow:none;box-shadow:none}.tooltip-inner{background-color:#708fa0;max-width:400px;text-align:left;font-weight:300;padding:2px 8px 4px;font-weight:bold;white-space:pre-wrap}.tooltip.top .tooltip-arrow{border-top-color:#708fa0}.tooltip.top-left .tooltip-arrow{border-top-color:#708fa0}.tooltip.top-right .tooltip-arrow{border-top-color:#708fa0}.tooltip.right .tooltip-arrow{border-right-color:#708fa0}.tooltip.left .tooltip-arrow{border-left-color:#708fa0}.tooltip.bottom .tooltip-arrow{border-bottom-color:#708fa0}.tooltip.bottom-left .tooltip-arrow{border-bottom-color:#708fa0}.tooltip.bottom-right .tooltip-arrow{border-bottom-color:#708fa0}.tooltip.in{opacity:1;filter:alpha(opacity=100)}table{margin-bottom:0 !important}table th{font-size:11px;color:#bebebe;font-weight:normal}table thead tr th{border:none !important}table .time{font-size:12px}table td a:hover{color:#6fdbe8}.table>thead>tr>th,.table>tbody>tr>th,.table>tfoot>tr>th,.table>thead>tr>td,.table>tbody>tr>td,.table>tfoot>tr>td{padding:10px 10px 10px 0}.table>thead>tr>th select,.table>tbody>tr>th select,.table>tfoot>tr>th select,.table>thead>tr>td select,.table>tbody>tr>td select,.table>tfoot>tr>td select{font-size:12px;padding:4px 8px;height:30px;margin:0}.comment-container{margin-top:10px}.comment .media{position:relative !important;margin-top:0}.comment .media .nav-pills.preferences{display:none;right:-3px;top:-3px}.comment .media-body{overflow:visible}.comment.guest-mode .media:last-child .wall-entry-controls{margin-bottom:0}.comment.guest-mode .media:last-child hr{display:none}.grid-view img{width:24px;height:24px}.grid-view .filters input,.grid-view .filters select{border:2px solid #ededed;box-shadow:none;border-radius:4px;font-size:12px;padding:4px}.grid-view .filters input:focus,.grid-view .filters select:focus{border:2px solid #6fdbe8;outline:0;box-shadow:none}.grid-view{padding:15px 0 0}.grid-view img{border-radius:3px}.grid-view table th{font-size:13px !important;font-weight:bold !important}.grid-view table td{vertical-align:middle !important}.grid-view table tr{font-size:13px !important}.grid-view table thead tr th:first-of-type{padding-left:5px}.grid-view table tbody tr{height:50px}.grid-view table tbody tr td:first-of-type{padding-left:5px}.grid-view .summary{font-size:12px;color:#bac2c7}.tags .tag{margin-top:5px;border-radius:2px;padding:4px 8px;text-transform:uppercase;max-width:150px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.placeholder{padding:15px}.placeholder-empty-stream{background-image:url("../img/placeholder-postform-arrow.png");background-repeat:no-repeat;padding:37px 0 0 70px;margin-left:90px}.media-list li.placeholder{font-size:14px !important;border-bottom:none}.media-list li.placeholder:hover{background:none !important;border-left:3px solid white}ul.tag_input{list-style:none;background-color:#fff;border:1px solid #ccc;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);-webkit-transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s;transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s;padding:0 0 9px 4px}ul.tag_input li img{margin:0 5px 0 0}.tag_input_field{outline:none;border:none !important;padding:5px 4px 0 !important;width:170px;margin:2px 0 0 !important}.userInput,.spaceInput{background-color:#6fdbe8;font-weight:600;color:#fff;border-radius:3px;font-size:12px !important;padding:2px;float:left;margin:3px 4px 0 0}.userInput i,.spaceInput i{padding:0 6px;font-size:14px;cursor:pointer;line-height:8px}.contentForm_options{margin-top:10px}.oembed_snippet{position:relative;padding-bottom:55%;padding-top:15px;height:0;overflow:hidden}.oembed_snippet iframe{position:absolute;top:0;left:0;width:100%;height:100%}#notification_overview_filter label{display:block}.activities{max-height:400px;overflow:auto}.activities li .media{position:relative}.activities li .media .img-space{position:absolute;top:14px;left:14px}.wall-entry{position:relative}.wall-entry .media{overflow:visible}.wall-entry .well{margin-bottom:0}.wall-entry .well .comment .show-all-link{font-size:12px;cursor:pointer}.wall-entry-controls{display:inline-block}.wall-entry-controls,.wall-entry-controls a{font-size:11px;color:#aeaeae;margin-top:10px;margin-bottom:0}.space-owner{text-align:center;margin:14px 0;font-size:13px;color:#999}.placeholder{padding:10px}.placeholder-empty-stream{background-image:url("../img/placeholder-postform-arrow.png");background-repeat:no-repeat;padding:37px 0 0 70px;margin-left:90px}ul.tag_input{-webkit-box-shadow:none;box-shadow:none;border:2px solid #ededed}ul.tag_input.focus{border:2px solid #6fdbe8;outline:0;box-shadow:none}.space-member-sign{color:#97d271;position:absolute;top:42px;left:42px;font-size:16px;background:#fff;width:24px;height:24px;padding:2px 3px 1px 4px;border-radius:50px;border:2px solid #97d271}.contentForm_options{min-height:29px}.contentForm-upload-progress{margin-top:18px;margin-bottom:10px !important}.btn_container{position:relative}.btn_container .label-public{position:absolute;right:40px;top:11px}.login-screen{margin-top:-70px}.mime{background-repeat:no-repeat;background-position:0 0;padding:1px 0 4px 26px}.mime-word{background-image:url("../img/mime/word.png")}.mime-excel{background-image:url("../img/mime/excel.png")}.mime-powerpoint{background-image:url("../img/mime/powerpoint.png")}.mime-pdf{background-image:url("../img/mime/pdf.png")}.mime-zip{background-image:url("../img/mime/zip.png")}.mime-image{background-image:url("../img/mime/image.png")}.mime-file{background-image:url("../img/mime/file.png")}.mime-photoshop{background-image:url("../img/mime/photoshop.png")}.mime-illustrator{background-image:url("../img/mime/illustrator.png")}.mime-video{background-image:url("../img/mime/video.png")}.mime-audio{background-image:url("../img/mime/audio.png")}.files,#postFormFiles_list{padding-left:0}.contentForm-upload-list{padding-left:0}.contentForm-upload-list li:first-child{margin-top:10px}.file_upload_remove_link,.file_upload_remove_link:hover{color:#ff8989;cursor:pointer}#contentFormError{color:#ff8989;padding-left:0;list-style:none}.post-files,.oembed_snippet{margin-top:10px}.post-files img{vertical-align:top;margin-bottom:3px;margin-right:5px;max-height:130px;-webkit-animation-duration:2s;animation-duration:2s}#wallStream.mobile .post-files{margin-top:10px;display:flex;overflow-x:auto}#wallStream.mobile .post-files img{max-width:190px}.comment_create,.content_edit{position:relative}.comment_create .comment-buttons,.content_edit .comment-buttons{position:absolute;top:2px;right:5px}.comment_create .btn-comment-submit,.content_edit .btn-comment-submit{margin-top:3px}.comment_create .fileinput-button,.content_edit .fileinput-button{float:left;padding:6px 10px;background:transparent !important}.comment_create .fileinput-button .fa,.content_edit .fileinput-button .fa{color:#d7d7d7}.comment_create .fileinput-button:hover .fa,.content_edit .fileinput-button:hover .fa{background:transparent !important;color:#b2b2b2}.comment_create .fileinput-button:active,.content_edit .fileinput-button:active{box-shadow:none !important}.upload-box-container{position:relative}.upload-box{display:none;position:absolute;top:0;left:0;width:100%;height:100px;padding:20px;background:#f8f8f8;border:2px dashed #ddd;text-align:center;border-radius:3px;cursor:pointer}.upload-box input[type="file"]{position:absolute;opacity:0}.upload-box .upload-box-progress-bar{display:none}.image-upload-container{position:relative}.image-upload-container .image-upload-buttons{display:none;position:absolute;right:5px;bottom:5px}.image-upload-container input[type="file"]{position:absolute;opacity:0}.image-upload-container .image-upload-loader{display:none;position:absolute;top:0;left:0;width:100%;height:100%;padding:20px;background:#f8f8f8}.langSwitcher{display:inline-block}.data-saved{padding-left:10px;color:#6fdbe8}.wallFilterPanel li{font-size:11px;font-weight:600}.wallFilterPanel li a{color:#555}.wallFilterPanel .dropdown-menu li{margin-bottom:0}.wallFilterPanel .dropdown-menu li a{font-size:12px}.wallFilterPanel .dropdown-menu li a:hover{color:#fff !important}ul.tour-list{list-style:none;margin-bottom:0;padding-left:10px}ul.tour-list li{padding-top:5px}ul.tour-list li a{color:#6fdbe8}ul.tour-list li a .fa{width:16px}ul.tour-list li.completed a{text-decoration:line-through;color:#bebebe}.powered,.powered a{color:#b8c7d3 !important}.errorMessage{color:#ff8989;padding:10px 0}.error{border-color:#ff8989 !important}.has-error .help-block,.has-error .control-label,.has-error .radio,.has-error .checkbox,.has-error .radio-inline,.has-error .checkbox-inline{color:#ff8989 !important}.has-error .form-control,.has-error .form-control:focus{border-color:#ff8989;-webkit-box-shadow:none;box-shadow:none}.has-success .help-block,.has-success .control-label,.has-success .radio,.has-success .checkbox,.has-success .radio-inline,.has-success .checkbox-inline{color:#97d271}.has-success .form-control,.has-success .form-control:focus{border-color:#97d271;-webkit-box-shadow:none;box-shadow:none}.has-warning .help-block,.has-warning .control-label,.has-warning .radio,.has-warning .checkbox,.has-warning .radio-inline,.has-warning .checkbox-inline{color:#fdd198}.has-warning .form-control,.has-warning .form-control:focus{border-color:#fdd198;-webkit-box-shadow:none;box-shadow:none}.highlight{background-color:#fff8e0}input.placeholder,textarea.placeholder{padding:0 0 0 10px;color:#999}.ekko-lightbox .modal-content .modal-body{padding:10px}.ekko-lightbox .modal-content .modal-footer{padding:0 10px 10px}.ekko-lightbox-container{position:relative}.ekko-lightbox-nav-overlay{position:absolute;top:0;left:0;z-index:100;width:100%;height:100%}.ekko-lightbox-nav-overlay a{z-index:100;display:block;width:49%;height:100%;font-size:30px;color:#fff;text-shadow:2px 2px 4px #000;opacity:0;outline:none;text-decoration:none;filter:dropshadow(color=#000000, offx=2, offy=2);-webkit-transition:opacity .5s;-moz-transition:opacity .5s;-o-transition:opacity .5s;transition:opacity .5s}.ekko-lightbox-nav-overlay a:hover{color:#708fa0}.ekko-lightbox-nav-overlay a:empty{width:49%}.ekko-lightbox a:hover{text-decoration:none;opacity:1}.ekko-lightbox .glyphicon-chevron-left{left:0;float:left;padding-left:15px;text-align:left}.ekko-lightbox .glyphicon-chevron-right{right:0;float:right;padding-right:15px;text-align:right}.atwho-view .cur{border-left:3px solid #6fdbe8;background-color:#f7f7f7 !important}.atwho-user,.atwho-space,.atwho-input a{color:#6fdbe8}.atwho-input a:hover{color:#6fdbe8}.atwho-view strong{background-color:#f9f0d2}.atwho-view .cur strong{background-color:#f9f0d2}.comment .jp-progress{background-color:#dbdcdd !important}.comment .jp-play-bar{background:#cacaca}.sk-spinner-three-bounce.sk-spinner{margin:0 auto;width:70px;text-align:center}.loader{padding:30px 0}.loader .sk-spinner-three-bounce div,.loader .sk-spinner-three-bounce span{width:12px;height:12px;background-color:#6fdbe8;border-radius:100%;display:inline-block;-webkit-animation:sk-threeBounceDelay 1.4s infinite ease-in-out;animation:sk-threeBounceDelay 1.4s infinite ease-in-out;-webkit-animation-fill-mode:both;animation-fill-mode:both}.loader .sk-spinner-three-bounce .sk-bounce1{-webkit-animation-delay:-0.32s;animation-delay:-0.32s}.loader .sk-spinner-three-bounce .sk-bounce2{-webkit-animation-delay:-0.16s;animation-delay:-0.16s}@-webkit-keyframes sk-threeBounceDelay{0%,80%,100%{-webkit-transform:scale(0);transform:scale(0)}40%{-webkit-transform:scale(1);transform:scale(1)}}@keyframes sk-threeBounceDelay{0%,80%,100%{-webkit-transform:scale(0);transform:scale(0)}40%{-webkit-transform:scale(1);transform:scale(1)}}.loader-modal{padding:8px 0}.loader-postform{padding:9px 0}.loader-postform .sk-spinner-three-bounce.sk-spinner{text-align:left;margin:0}.modal-dialog.fadeIn,.modal-dialog.pulse{-webkit-animation-duration:200ms;-moz-animation-duration:200ms;animation-duration:200ms}.panel.pulse,.panel.fadeIn{-webkit-animation-duration:200ms;-moz-animation-duration:200ms;animation-duration:200ms}img.bounceIn{-webkit-animation-duration:800ms;-moz-animation-duration:800ms;animation-duration:800ms}.markdown-render h1,.markdown-render h2,.markdown-render h3,.markdown-render h4,.markdown-render h5,.markdown-render h6{font-weight:bold !important}.markdown-render h1{font-size:28px !important}.markdown-render h2{font-size:24px !important}.markdown-render h3{font-size:18px !important}.markdown-render h4{font-size:16px !important}.markdown-render h5{font-size:14px !important}.markdown-render h6{color:#999;font-size:14px !important}.markdown-render pre{padding:0;border:none;border-radius:3px}.markdown-render pre code{padding:10px;border-radius:3px;font-size:12px !important}.markdown-render a,.markdown-render a:visited{background-color:inherit;text-decoration:none;color:#6fdbe8 !important}.markdown-render img{max-width:100%;display:table-cell !important}.markdown-render table{width:100%}.markdown-render table th{font-size:13px;font-weight:700;color:#555}.markdown-render table thead tr{border-bottom:1px solid #d7d7d7}.markdown-render table tbody tr td,.markdown-render table thead tr th{border:1px solid #d7d7d7 !important;padding:4px}.md-editor.active{border:2px solid #6fdbe8 !important}.md-editor textarea{padding:10px !important}@media (max-width:767px){.topbar{padding-left:0;padding-right:0}#space-menu>.title{display:none}#space-menu-dropdown{width:300px !important}#search-menu-dropdown{width:300px !important;max-height:250px !important}.notifications{position:inherit !important;float:left !important}.notifications .dropdown-menu{width:300px !important;margin-left:0 !important}.notifications .dropdown-menu .arrow{margin-left:-142px !important}.panel-profile-controls{padding-left:0 !important;padding-top:50px}.panel-profile .panel-profile-header .img-profile-data h1{font-size:20px !important}}@media (max-width:991px){.controls-header{text-align:left !important}.list-group{margin-left:4px}.list-group-item{display:inline-block !important;border-radius:3px !important;margin:4px 0;margin-bottom:4px !important}.list-group-item{border:none !important}a.list-group-item:hover,a.list-group-item.active,a.list-group-item.active:hover,a.list-group-item.active:focus{border:none !important;background:#708fa0 !important;color:#fff !important}.layout-sidebar-container{display:none}}.onoffswitch-inner:before{background-color:#6fdbe8;color:#fff}.onoffswitch-inner:after{background-color:#d7d7d7;color:#999;text-align:right}.regular-checkbox:checked+.regular-checkbox-box{border:2px solid #6fdbe8;background:#6fdbe8;color:white}.regular-radio:checked+.regular-radio-button:after{background:#6fdbe8}.regular-radio:checked+.regular-radio-button{background-color:none;color:#99a1a7;border:2px solid #d7d7d7;margin-right:5px}.ui-widget-header{border:none !important;background:#fff !important;color:#7a7a7a !important;font-weight:300 !important}.ui-widget-content{border:1px solid #dddcda !important;border-radius:0 !important;background:#fff;color:#555 !important;-webkit-box-shadow:0 6px 6px rgba(0,0,0,0.1);box-shadow:0 6px 6px rgba(0,0,0,0.1)}.ui-datepicker .ui-datepicker-prev span,.ui-datepicker .ui-datepicker-next span{opacity:.2}.ui-datepicker .ui-datepicker-prev:hover,.ui-datepicker .ui-datepicker-next:hover{background:#fff !important;border:none;margin:1px}.ui-state-default,.ui-widget-content .ui-state-default,.ui-widget-header .ui-state-default{border:none !important;background:#f7f7f7 !important;color:#7a7a7a !important}.ui-state-highlight,.ui-widget-content .ui-state-highlight,.ui-widget-header .ui-state-highlight{border:none !important;border:1px solid #b2b2b2 !important}.ui-state-active,.ui-widget-content .ui-state-active,.ui-widget-header .ui-state-active{border:1px solid #6fdbe8 !important;background:#ddf6fa !important}#nprogress .bar{height:2px;background:#6fdbe8}.status-bar-body{color:white;position:fixed;width:100%;background-color:rgba(0,0,0,0.7);text-align:center;padding:20px;z-index:9999999;bottom:0;display:block;line-height:20px}.status-bar-close{color:white;fonfont-weight:bold;font-size:21px;cursor:pointer}.status-bar-close:hover{color:white}.status-bar-close i{vertical-align:top !important;padding-top:3px}.status-bar-content i{margin-right:10px;font-size:21px;vertical-align:middle}.status-bar-content .showMore{color:#6fdbe8;float:right;margin-left:10px;font-size:.7em;cursor:pointer;vertical-align:middle;white-space:nowrap}.status-bar-content .status-bar-details{text-align:left;font-size:.7em;margin-top:20px;max-height:200px;overflow:auto}.status-bar-content span{vertical-align:middle}.status-bar-content i.error,.status-bar-content i.fatal{color:#ff8989}.status-bar-content i.warning{color:#fdd198}.status-bar-content i.info,.status-bar-content i.debug{color:#6fdbe8}.status-bar-content i.success{color:#85CA2B}.stream-entry-loader{float:right;margin-top:5px}#space-menu-dropdown i.type{font-size:16px;color:#BFBFBF}#space-menu-spaces [data-space-chooser-item]{cursor:pointer}#space-menu-dropdown .input-group-addon{border-radius:0 4px 4px 0}#space-menu-dropdown .input-group-addon.focus{border-radius:0 4px 4px 0;border:2px solid #6fdbe8;border-left:0}#space-menu-search{border-right:0}#space-directory-link i{margin-right:0}.file-preview-content{cursor:pointer} \ No newline at end of file diff --git a/themes/HumHub/img/dynamic.php b/themes/HumHub/img/dynamic.php deleted file mode 100644 index 591ea9e00d..0000000000 --- a/themes/HumHub/img/dynamic.php +++ /dev/null @@ -1,55 +0,0 @@ - - array ( - 'db' => - array ( - 'class' => 'yii\\db\\Connection', - 'dsn' => 'mysql:host=127.0.0.1;dbname=installer', - 'username' => 'root', - 'password' => 'root', - 'charset' => 'utf8', - ), - 'user' => - array ( - ), - 'mailer' => - array ( - 'transport' => - array ( - 'class' => 'Swift_MailTransport', - ), - ), - 'view' => - array ( - 'theme' => - array ( - 'name' => 'HumHub', - ), - ), - 'formatter' => - array ( - 'defaultTimeZone' => 'Europe/Berlin', - ), - 'formatterApp' => - array ( - 'defaultTimeZone' => 'Europe/Berlin', - 'timeZone' => 'Europe/Berlin', - ), - ), - 'params' => - array ( - 'installer' => - array ( - 'db' => - array ( - 'installer_hostname' => '127.0.0.1', - 'installer_database' => 'installer', - ), - ), - 'config_created_at' => 1440430541, - 'installed' => true, - ), - 'name' => 'Installer', - 'language' => 'de', - 'timeZone' => 'Europe/Berlin', -); ?> \ No newline at end of file diff --git a/themes/HumHub/img/wmd-buttons.png b/themes/HumHub/img/wmd-buttons.png new file mode 100644 index 0000000000000000000000000000000000000000..50b37090363e6757e7bd0ba75cd1e0dfaabd13d2 GIT binary patch literal 7465 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