Resurrect the old alpha Koenig editor (#916)

requires https://github.com/TryGhost/Ghost/pull/9277

- added a `koenigEditor` feature flag
  - modified the feature service to accept a `developer` boolean on the options object passed into the internal `feature` method, if `true` the feature flag won't be enabled unless the `enableDeveloperExperiments` config option is also enabled
  - added "developer feature testing" section in labs that's only visible if `enableDeveloperExperiments` config flag is enabled
  - added koenig editor toggle to the developer section in labs

- enabled a switch between the markdown and koenig editors
  - modified the default value of the `mobiledoc` attr in the Post model to be a blank mobiledoc or blank markdown mobiledoc depending on the feature flag
  - modified the `autofocus` switch in editor controller's `setPost` method so that it is always switched, even for new->edit where the post model isn't swapped
  - added a compatibility check to the editor controller's `setPost` method that shows an alert and force enables the koenig editor if the koenig flag is not enabled and the opened post is not compatible with the markdown editor

- fixed various issues that have appeared due to the old koenig alpha becoming out of sync with master
This commit is contained in:
Kevin Ansfield 2018-01-18 15:36:01 +00:00 committed by GitHub
parent 4fc762c1c4
commit 506b2a9388
23 changed files with 305 additions and 156 deletions

View File

@ -99,7 +99,7 @@ export default Component.extend({
},
_setHeaderClass() {
let $editorTitle = this.$('.gh-editor-title');
let $editorTitle = this.$('.gh-editor-title, .kg-title-input');
let smallHeaderClass = 'gh-editor-header-small';
if (this.get('isSplitScreen')) {

View File

@ -77,6 +77,7 @@ const messageMap = {
export default Controller.extend({
application: controller(),
feature: service(),
notifications: service(),
router: service(),
slugGenerator: service(),
@ -89,6 +90,10 @@ export default Controller.extend({
showDeletePostModal: false,
showLeaveEditorModal: false,
showReAuthenticateModal: false,
useKoenig: false,
// koenig related properties
wordcount: 0,
/* private properties ----------------------------------------------------*/
@ -240,6 +245,11 @@ export default Controller.extend({
toggleReAuthenticateModal() {
this.toggleProperty('showReAuthenticateModal');
},
// TODO: this should be part of the koenig component
setWordcount(count) {
this.set('wordcount', count);
}
},
@ -472,7 +482,27 @@ export default Controller.extend({
// called by the new/edit routes to change the post model
setPost(post) {
// don't do anything if the post is the same
// switch between markdown/koenig depending on feature flag and post
// compatibility
let koenigEnabled = this.get('feature.koenigEditor');
let postIsMarkdownCompatible = post.isCompatibleWithMarkdownEditor();
if (koenigEnabled || !postIsMarkdownCompatible) {
this.set('useKoenig', true);
// display an alert if koenig is disabled but we use it anyway
// because the post is incompatible with the markdown editor
if (!koenigEnabled) {
alert('This post will be opened with the Koenig editor because it\'s not compatible with the markdown editor');
}
} else {
this.set('useKoenig', false);
}
// autofocus the editor if we have a new post, this also acts as a
// change signal to the persistent editor on new->edit
this.set('shouldFocusEditor', post.get('isNew'));
// don't do anything else if we're setting the same post
if (post === this.get('post')) {
return;
}
@ -482,9 +512,6 @@ export default Controller.extend({
this.set('post', post);
// only autofocus the editor if we have a new post
this.set('shouldFocusEditor', post.get('isNew'));
// need to set scratch values because they won't be present on first
// edit of the post
// TODO: can these be `boundOneWay` on the model as per the other attrs?
@ -641,10 +668,16 @@ export default Controller.extend({
}
// scratch isn't an attr so needs a manual dirty check
let mobiledoc = JSON.stringify(post.get('mobiledoc'));
let scratch = JSON.stringify(post.get('scratch'));
if (scratch !== mobiledoc) {
return true;
let mobiledoc = post.get('mobiledoc');
let scratch = post.get('scratch');
// additional guard in case we are trying to compare null with undefined
if (scratch || mobiledoc) {
let mobiledocJSON = JSON.stringify(mobiledoc);
let scratchJSON = JSON.stringify(scratch);
if (scratchJSON !== mobiledocJSON) {
return true;
}
}
// new+unsaved posts always return `hasDirtyAttributes: true`

View File

@ -4,7 +4,8 @@ import ValidationEngine from 'ghost-admin/mixins/validation-engine';
import attr from 'ember-data/attr';
import boundOneWay from 'ghost-admin/utils/bound-one-way';
import moment from 'moment';
import {BLANK_DOC} from 'ghost-admin/components/gh-markdown-editor';
import {BLANK_DOC as BLANK_MARKDOWN} from 'ghost-admin/components/gh-markdown-editor';
import {BLANK_DOC as BLANK_MOBILEDOC} from 'gh-koenig/components/gh-koenig';
import {belongsTo, hasMany} from 'ember-data/relationships';
import {compare} from '@ember/utils';
import {computed} from '@ember/object';
@ -69,6 +70,7 @@ function publishedAtCompare(postA, postB) {
export default Model.extend(Comparable, ValidationEngine, {
config: service(),
feature: service(),
ghostPaths: service(),
clock: service(),
settings: service(),
@ -93,7 +95,7 @@ export default Model.extend(Comparable, ValidationEngine, {
locale: attr('string'),
metaDescription: attr('string'),
metaTitle: attr('string'),
mobiledoc: attr('json-string', {defaultValue: () => BLANK_DOC}),
mobiledoc: attr('json-string'),
page: attr('boolean', {defaultValue: false}),
plaintext: attr('string'),
publishedAtUTC: attr('moment-utc'),
@ -113,6 +115,28 @@ export default Model.extend(Comparable, ValidationEngine, {
async: false
}),
init() {
// we can't use the defaultValue property on the attr because it won't
// have access to `this` for the feature check so we do it manually here.
if (!this.get('mobiledoc')) {
let defaultValue;
if (this.get('feature.koenigEditor')) {
defaultValue = BLANK_MOBILEDOC;
} else {
defaultValue = BLANK_MARKDOWN;
}
// using this.set() adds the property to the changedAttributes list
// which means the editor always sees new posts as dirty. Assigning
// the value directly works around that so you can exit the editor
// without a warning
this.mobiledoc = defaultValue;
}
this._super(...arguments);
},
scratch: null,
titleScratch: null,
@ -313,5 +337,25 @@ export default Model.extend(Comparable, ValidationEngine, {
let publishedAtBlogTZ = this.get('publishedAtBlogTZ');
let publishedAtUTC = publishedAtBlogTZ ? publishedAtBlogTZ.utc() : null;
this.set('publishedAtUTC', publishedAtUTC);
},
// the markdown editor expects a very specific mobiledoc format, if it
// doesn't match then we'll need to handle it by forcing Koenig
isCompatibleWithMarkdownEditor() {
let mobiledoc = this.get('mobiledoc');
if (
mobiledoc.markups.length === 0
&& mobiledoc.cards.length === 1
&& mobiledoc.cards[0][0] === 'card-markdown'
&& mobiledoc.sections.length === 1
&& mobiledoc.sections[0].length === 2
&& mobiledoc.sections[0][0] === 10
&& mobiledoc.sections[0][1] === 0
) {
return true;
}
return false;
}
});

View File

@ -12,18 +12,24 @@ export function feature(name, options = {}) {
return computed.apply(Ember, watchedProps.concat({
get() {
let enabled = false;
if (user) {
return this.get(`accessibility.${name}`);
enabled = this.get(`accessibility.${name}`);
} else if (this.get(`config.${name}`)) {
enabled = this.get(`config.${name}`);
} else {
enabled = this.get(`labs.${name}`) || false;
}
if (this.get(`config.${name}`)) {
return this.get(`config.${name}`);
if (options.developer) {
enabled = enabled && this.get('config.enableDeveloperExperiments');
}
return this.get(`labs.${name}`) || false;
return enabled;
},
set(key, value) {
this.update(key, value, user);
this.update(key, value, options);
if (onChange) {
// value must be passed here because the value isn't set until
@ -44,6 +50,7 @@ export default Service.extend({
notifications: service(),
lazyLoader: service(),
koenigEditor: feature('koenigEditor', {developer: true}),
publicAPI: feature('publicAPI'),
subscribers: feature('subscribers'),
nightShift: feature('nightShift', {user: true, onChange: '_setAdminTheme'}),
@ -80,9 +87,9 @@ export default Service.extend({
});
},
update(key, value, user = false) {
let serviceProperty = user ? 'accessibility' : 'labs';
let model = this.get(user ? '_user' : 'settings');
update(key, value, options = {}) {
let serviceProperty = options.user ? 'accessibility' : 'labs';
let model = this.get(options.user ? '_user' : 'settings');
let featureObject = this.get(serviceProperty);
// set the new key value for either the labs property or the accessibility property
@ -102,7 +109,7 @@ export default Service.extend({
// we'll always have an errors object unless we hit a
// validation error
if (!error) {
throw new EmberError(`Validation of the feature service ${user ? 'user' : 'settings'} model failed when updating ${serviceProperty}.`);
throw new EmberError(`Validation of the feature service ${options.user ? 'user' : 'settings'} model failed when updating ${serviceProperty}.`);
}
this.get('notifications').showAPIError(error);

View File

@ -1,6 +1,6 @@
@import "koenig-toolbar.css";
@import "koenig-menu.css";
@import "../ghost-editor/cardmenu.css";
@import "koenig-cardmenu.css";
.gh-koenig-container {
height: 100%;

View File

@ -56,7 +56,7 @@
/* Addons: gh-koenig
/* ---------------------------------------------------------- */
/*@import "addons/gh-koenig/gh-koenig.css";*/
@import "addons/gh-koenig/gh-koenig.css";
:root {

View File

@ -56,7 +56,7 @@
/* Addons: gh-koenig
/* ---------------------------------------------------------- */
/*@import "addons/gh-koenig/gh-koenig.css";*/
@import "addons/gh-koenig/gh-koenig.css";
/* ---------------------------✈️----------------------------- */

View File

@ -32,100 +32,144 @@
</section>
</header>
{{!--
NOTE: title is part of the markdown editor container so that it has
access to the markdown editor's "focus" action
--}}
{{#gh-markdown-editor
tabindex="2"
placeholder="Begin writing your story..."
autofocus=shouldFocusEditor
uploadedImageUrls=editor.uploadedImageUrls
mobiledoc=(readonly post.scratch)
isFullScreen=editor.isFullScreen
onChange=(action "updateScratch")
onFullScreenToggle=(action editor.toggleFullScreen)
onPreviewToggle=(action editor.togglePreview)
onSplitScreenToggle=(action editor.toggleSplitScreen)
onImageFilesSelected=(action editor.uploadImages)
imageMimeTypes=editor.imageMimeTypes
as |markdown|
}}
<div class="gh-markdown-editor-pane">
{{gh-textarea post.titleScratch
class="gh-editor-title"
placeholder="Post Title"
tabindex="1"
autoExpand=".gh-markdown-editor-pane"
update=(action "updateTitleScratch")
focusOut=(action (perform saveTitle))
keyEvents=(hash
9=(action markdown.focus 'bottom')
13=(action markdown.focus 'top')
)
data-test-editor-title-input=true
}}
{{markdown.editor}}
</div>
{{#if useKoenig}}
<div class="gh-editor-container needsclick">
<div class="gh-editor-inner">
{{!--
NOTE: the mobiledoc property is unbound so that the setting the
serialized version onChange doesn't cause a deserialization and
re-render of the editor on every key press / editor change
{{#if markdown.isSplitScreen}}
<div class="gh-markdown-editor-preview">
<h1 class="gh-markdown-editor-preview-title">{{post.titleScratch}}</h1>
<div class="gh-markdown-editor-preview-content"></div>
TODO: note above is no longer correct, changed to readonly to
fix a persistent editor content bug that occurred due to the
editor not being re-rendered on edit->new transition.
Needs perf investigation!
--}}
{{#gh-koenig
mobiledoc=(readonly model.scratch)
onChange=(action "updateScratch")
autofocus=shouldFocusEditor
tabindex="2"
titleSelector="#kg-title-input"
containerSelector=".gh-editor-container"
wordcountDidChange=(action "setWordcount")
as |koenig|
}}
{{koenig-title-input
id="koenig-title-input"
val=(readonly model.titleScratch)
onChange=(action "updateTitleScratch")
tabindex="1"
autofocus=shouldFocusTitle
focusOut=(action (perform saveTitle))
editor=(readonly koenig.editor)
editorHasRendered=koenig.hasRendered
editorMenuIsOpen=koenig.isMenuOpen
}}
{{/gh-koenig}}
</div>
</div>
<div class="gh-editor-wordcount">{{pluralize wordcount 'word'}}.</div>
{{else}}
{{!--
NOTE: title is part of the markdown editor container so that it has
access to the markdown editor's "focus" action
--}}
{{#gh-markdown-editor
tabindex="2"
placeholder="Begin writing your story..."
autofocus=shouldFocusEditor
uploadedImageUrls=editor.uploadedImageUrls
mobiledoc=(readonly post.scratch)
isFullScreen=editor.isFullScreen
onChange=(action "updateScratch")
onFullScreenToggle=(action editor.toggleFullScreen)
onPreviewToggle=(action editor.togglePreview)
onSplitScreenToggle=(action editor.toggleSplitScreen)
onImageFilesSelected=(action editor.uploadImages)
imageMimeTypes=editor.imageMimeTypes
as |markdown|
}}
<div class="gh-markdown-editor-pane">
{{gh-textarea post.titleScratch
class="gh-editor-title"
placeholder="Post Title"
tabindex="1"
autoExpand=".gh-markdown-editor-pane"
update=(action "updateTitleScratch")
focusOut=(action (perform saveTitle))
keyEvents=(hash
9=(action markdown.focus 'bottom')
13=(action markdown.focus 'top')
)
data-test-editor-title-input=true
}}
{{markdown.editor}}
</div>
{{#if markdown.isSplitScreen}}
<div class="gh-markdown-editor-preview">
<h1 class="gh-markdown-editor-preview-title">{{post.titleScratch}}</h1>
<div class="gh-markdown-editor-preview-content"></div>
</div>
{{/if}}
{{gh-tour-item "using-the-editor"
target=".gh-editor-footer"
throbberAttachment="top left"
throbberOffset="0 20%"
popoverTriangleClass="bottom"
}}
{{/gh-markdown-editor}}
{{!-- TODO: put tool/status bar in here so that scroll area can be fixed --}}
<footer class="gh-editor-footer"></footer>
{{!-- files are dragged over editor pane --}}
{{#if editor.isDraggedOver}}
<div class="drop-target gh-editor-drop-target">
<div class="drop-target-message">
<h3>Drop image(s) here to upload</h3>
</div>
</div>
{{/if}}
{{gh-tour-item "using-the-editor"
target=".gh-editor-footer"
throbberAttachment="top left"
throbberOffset="0 20%"
popoverTriangleClass="bottom"
}}
{{/gh-markdown-editor}}
{{!-- files have been dropped ready to be uploaded --}}
{{#if editor.droppedFiles}}
{{#gh-uploader
files=editor.droppedFiles
accept=editor.imageMimeTypes
extensions=editor.imageExtensions
onComplete=(action editor.uploadComplete)
onCancel=(action editor.uploadCancelled)
as |upload|
}}
<div class="gh-editor-image-upload {{if upload.errors "-error"}}">
<div class="gh-editor-image-upload-content">
{{#if upload.errors}}
<h3>Upload failed</h3>
{{!-- TODO: put tool/status bar in here so that scroll area can be fixed --}}
<footer class="gh-editor-footer"></footer>
{{#each upload.errors as |error|}}
<div class="failed">{{error.fileName}} - {{error.message}}</div>
{{/each}}
{{!-- files are dragged over editor pane --}}
{{#if editor.isDraggedOver}}
<div class="drop-target gh-editor-drop-target">
<div class="drop-target-message">
<h3>Drop image(s) here to upload</h3>
</div>
</div>
{{/if}}
<button class="gh-btn gh-btn-grey gh-btn-icon" {{action upload.cancel}}>
<span>{{inline-svg "close"}} Close</span>
</button>
{{else}}
{{!-- files have been dropped ready to be uploaded --}}
{{#if editor.droppedFiles}}
{{#gh-uploader
files=editor.droppedFiles
accept=editor.imageMimeTypes
extensions=editor.imageExtensions
onComplete=(action editor.uploadComplete)
onCancel=(action editor.uploadCancelled)
as |upload|
}}
<div class="gh-editor-image-upload {{if upload.errors "-error"}}">
<div class="gh-editor-image-upload-content">
{{#if upload.errors}}
<h3>Upload failed</h3>
{{#each upload.errors as |error|}}
<div class="failed">{{error.fileName}} - {{error.message}}</div>
{{/each}}
<button class="gh-btn gh-btn-grey gh-btn-icon" {{action upload.cancel}}>
<span>{{inline-svg "close"}} Close</span>
</button>
{{else}}
<h3>Uploading {{pluralize upload.files.length "image"}}...</h3>
{{upload.progressBar}}
{{/if}}
<h3>Uploading {{pluralize upload.files.length "image"}}...</h3>
{{upload.progressBar}}
{{/if}}
</div>
</div>
</div>
{{/gh-uploader}}
{{/if}}
{{/gh-uploader}}
{{/if}}
{{/if}} {{!-- end Koenig conditional --}}
{{/gh-editor}}
{{#if showDeletePostModal}}

View File

@ -155,6 +155,21 @@
{{/gh-uploader}}
</div>
{{#if config.enableDeveloperExperiments}}
<div class="gh-setting-header">⚠️ Developer-only Feature Testing ⚠️</div>
<div class="gh-setting">
<div class="gh-setting-content">
<div class="gh-setting-title">Koenig Editor</div>
<div class="gh-setting-desc">
Highly experimental (i.e. broken) editor. For developer use only.<br>
<strong>Warning:</strong> Stories created or edited with Koenig will no longer be compatible with the old markdown editor.</div>
</div>
<div class="gh-setting-action">
<div class="for-checkbox">{{gh-feature-flag "koenigEditor"}}</div>
</div>
</div>
{{/if}}
</section>
</section>

View File

@ -149,6 +149,9 @@ module.exports = function (defaults) {
'jquery-deparam': {
import: ['jquery-deparam.js']
},
'mobiledoc-kit': {
import: ['dist/amd/mobiledoc-kit.js', 'dist/amd/mobiledoc-kit.map']
},
'password-generator': {
import: ['lib/password-generator.js']
},

View File

@ -1,5 +1,6 @@
import Component from '@ember/component';
import counter from 'ghost-admin/utils/word-count';
import formatMarkdown from 'ghost-admin/utils/format-markdown';
import layout from '../../templates/components/card-markdown';
import {
UnsupportedMediaTypeError,
@ -8,7 +9,6 @@ import {
isVersionMismatchError
} from 'ghost-admin/services/ajax';
import {computed} from '@ember/object';
import {formatMarkdown} from '../../lib/format-markdown';
import {invokeAction} from 'ember-invoke-action';
import {isBlank} from '@ember/utils';
import {isArray as isEmberArray} from '@ember/array';
@ -25,7 +25,7 @@ export default Component.extend({
extensions: null,
preview: computed('value', function () {
return formatMarkdown([this.get('payload').markdown]);
return formatMarkdown(this.get('payload').markdown);
}),
// TODO: remove observer
@ -47,7 +47,7 @@ export default Component.extend({
didReceiveAttrs() {
if (!this.get('isEditing')) {
this.set('preview', formatMarkdown([this.get('payload').markdown]));
this.set('preview', formatMarkdown(this.get('payload').markdown));
} else {
run.next(() => {
this.$('textarea').focus();

View File

@ -72,7 +72,7 @@ export default Component.extend({
cards = defaultCards.concat(cards).map(card => createCard(card));
// add our default atoms
atoms.concat([{
atoms = atoms.concat([{
name: 'soft-return',
type: 'dom',
render() {
@ -117,6 +117,15 @@ export default Component.extend({
this._startedRunLoop = false;
},
didReceiveAttrs() {
this._super(...arguments);
if (this.get('autofocus') !== this._autofocus) {
this._autofocus = this.get('autofocus');
this._hasAutofocused = false;
}
},
willRender() {
// Use a default mobiledoc. If there are no changes, then return early.
let mobiledoc = this.get('mobiledoc') || BLANK_DOC;
@ -235,7 +244,7 @@ export default Component.extend({
// the first lot of content is entered and we expect the caret to be at
// the end of the document.
// TODO: can this be removed if we refactor the new/edit screens to not re-render?
if (this.get('autofocus')) {
if (this._autofocus && !this._hasAutofocused) {
let range = document.createRange();
range.selectNodeContents(this.editor.element);
range.collapse(false);
@ -243,6 +252,10 @@ export default Component.extend({
sel.removeAllRanges();
sel.addRange(range);
editor._ensureFocus(); // PRIVATE API
// ensure we don't run the autofocus more than once between
// `autofocus` attr changes
this._hasAutofocused = true;
}
this.processWordcount();

View File

@ -185,7 +185,7 @@ export default Component.extend({
// if the current paragraph is empty then the position is 0
if (!cursorPositionOnScreen || cursorPositionOnScreen.top === 0) {
if (editor.activeSection.renderNode) {
if (editor.activeSection && editor.activeSection.renderNode) {
cursorPositionOnScreen = editor.activeSection.renderNode.element.getBoundingClientRect();
} else {
this.setCursorAtOffset(0);

View File

@ -1,34 +0,0 @@
/* global Showdown, html_sanitize*/
import cajaSanitizers from './caja-sanitizers';
import {helper} from '@ember/component/helper';
import {htmlSafe} from '@ember/string';
// eslint-disable-next-line new-cap
let showdown = new Showdown.converter({extensions: ['ghostgfm', 'footnotes', 'highlight']});
export function formatMarkdown(params) {
if (!params || !params.length) {
return;
}
let markdown = params[0] || '';
let escapedhtml = '';
// convert markdown to HTML
escapedhtml = showdown.makeHtml(markdown);
// replace script and iFrame
escapedhtml = escapedhtml.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi,
'<pre class="js-embed-placeholder">Embedded JavaScript</pre>');
escapedhtml = escapedhtml.replace(/<iframe\b[^<]*(?:(?!<\/iframe>)<[^<]*)*<\/iframe>/gi,
'<pre class="iframe-embed-placeholder">Embedded iFrame</pre>');
// sanitize html
// jscs:disable requireCamelCaseOrUpperCaseIdentifiers
escapedhtml = html_sanitize(escapedhtml, cajaSanitizers.url, cajaSanitizers.id);
// jscs:enable requireCamelCaseOrUpperCaseIdentifiers
return htmlSafe(escapedhtml);
}
export default helper(formatMarkdown);

View File

@ -4,9 +4,7 @@
ondrop={{action "didDrop"}}
ondragover={{action "didDragOver"}}
ondragleave={{action "didDragLeave"}}
>
{{value}}
</textarea>
>{{value}}</textarea>
{{else}}
{{{preview}}}
{{/if}}

View File

@ -6,7 +6,7 @@
],
"readmeFilename": "README.md",
"dependencies": {
"ember-cli-babel": "5.2.4",
"ember-cli-htmlbars": "1.1.1"
"ember-cli-babel": "^6.11.0",
"ember-cli-htmlbars": "^2.0.3"
}
}

View File

@ -110,6 +110,7 @@
"markdown-it-lazy-headers": "0.1.3",
"markdown-it-mark": "2.0.0",
"matchdep": "2.0.0",
"mobiledoc-kit": "0.10.20",
"password-generator": "2.2.0",
"postcss-color-function": "4.0.1",
"postcss-custom-properties": "6.2.0",
@ -121,7 +122,8 @@
},
"ember-addon": {
"paths": [
"lib/asset-delivery"
"lib/asset-delivery",
"lib/gh-koenig"
]
}
}

View File

@ -126,7 +126,7 @@ describe('Acceptance: Editor', function () {
await datepickerSelect('[data-test-date-time-picker-datepicker]', validTime);
// hide psm
await click('[data-test-psm-trigger]');
await click('[data-test-close-settings-menu]');
// checking the flow of the saving button for a draft
expect(

View File

@ -11,6 +11,7 @@ describe('Unit: Controller: editor', function () {
setupTest('controller:editor', {
needs: [
'controller:application',
'service:feature',
'service:notifications',
// 'service:router',
'service:slugGenerator',

View File

@ -9,10 +9,13 @@ describe('Unit: Model: post', function () {
'model:user',
'model:tag',
'model:role',
'service:ajax',
'service:clock',
'service:config',
'service:feature',
'service:ghostPaths',
'service:lazyLoader',
'service:notifications',
'service:session',
'service:settings'
]

View File

@ -10,9 +10,14 @@ describe('Unit: Serializer: post', function () {
'transform:json-string',
'model:user',
'model:tag',
'service:ajax',
'service:clock',
'service:config',
'service:feature',
'service:ghostPaths',
'service:lazyLoader',
'service:notifications',
'service:session',
'service:settings'
]
});

View File

@ -6835,6 +6835,21 @@ mktemp@~0.4.0:
version "0.4.0"
resolved "https://registry.yarnpkg.com/mktemp/-/mktemp-0.4.0.tgz#6d0515611c8a8c84e484aa2000129b98e981ff0b"
mobiledoc-dom-renderer@0.6.5:
version "0.6.5"
resolved "https://registry.yarnpkg.com/mobiledoc-dom-renderer/-/mobiledoc-dom-renderer-0.6.5.tgz#56c0302c4f9c30840ab5b9b20dfe905aed1e437b"
mobiledoc-kit@0.10.20:
version "0.10.20"
resolved "https://registry.yarnpkg.com/mobiledoc-kit/-/mobiledoc-kit-0.10.20.tgz#2925a6223bac2a1eeb3468a4a94d992618089312"
dependencies:
mobiledoc-dom-renderer "0.6.5"
mobiledoc-text-renderer "0.3.2"
mobiledoc-text-renderer@0.3.2:
version "0.3.2"
resolved "https://registry.yarnpkg.com/mobiledoc-text-renderer/-/mobiledoc-text-renderer-0.3.2.tgz#126a167a6cf8b6cd7e58c85feb18043603834580"
mocha@^2.5.3:
version "2.5.3"
resolved "https://registry.yarnpkg.com/mocha/-/mocha-2.5.3.tgz#161be5bdeb496771eb9b35745050b622b5aefc58"