Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'app/assets/javascripts/static_site_editor')
-rw-r--r--app/assets/javascripts/static_site_editor/components/edit_area.vue43
-rw-r--r--app/assets/javascripts/static_site_editor/components/edit_drawer.vue32
-rw-r--r--app/assets/javascripts/static_site_editor/components/front_matter_controls.vue57
-rw-r--r--app/assets/javascripts/static_site_editor/components/publish_toolbar.vue16
-rw-r--r--app/assets/javascripts/static_site_editor/services/formatter.js44
-rw-r--r--app/assets/javascripts/static_site_editor/services/image_service.js1
-rw-r--r--app/assets/javascripts/static_site_editor/services/parse_source_file.js66
7 files changed, 208 insertions, 51 deletions
diff --git a/app/assets/javascripts/static_site_editor/components/edit_area.vue b/app/assets/javascripts/static_site_editor/components/edit_area.vue
index 53fbb2a330d..e602f26acdf 100644
--- a/app/assets/javascripts/static_site_editor/components/edit_area.vue
+++ b/app/assets/javascripts/static_site_editor/components/edit_area.vue
@@ -2,6 +2,7 @@
import RichContentEditor from '~/vue_shared/components/rich_content_editor/rich_content_editor.vue';
import PublishToolbar from './publish_toolbar.vue';
import EditHeader from './edit_header.vue';
+import EditDrawer from './edit_drawer.vue';
import UnsavedChangesConfirmDialog from './unsaved_changes_confirm_dialog.vue';
import parseSourceFile from '~/static_site_editor/services/parse_source_file';
import { EDITOR_TYPES } from '~/vue_shared/components/rich_content_editor/constants';
@@ -15,6 +16,7 @@ export default {
RichContentEditor,
PublishToolbar,
EditHeader,
+ EditDrawer,
UnsavedChangesConfirmDialog,
},
props: {
@@ -48,6 +50,8 @@ export default {
parsedSource: parseSourceFile(this.preProcess(true, this.content)),
editorMode: EDITOR_TYPES.wysiwyg,
isModified: false,
+ hasMatter: false,
+ isDrawerOpen: false,
};
},
imageRepository: imageRepository(),
@@ -55,10 +59,19 @@ export default {
editableContent() {
return this.parsedSource.content(this.isWysiwygMode);
},
+ editableMatter() {
+ return this.isDrawerOpen ? this.parsedSource.matter() : {};
+ },
+ hasSettings() {
+ return this.hasMatter && this.isWysiwygMode;
+ },
isWysiwygMode() {
return this.editorMode === EDITOR_TYPES.wysiwyg;
},
},
+ created() {
+ this.refreshEditHelpers();
+ },
methods: {
preProcess(isWrap, value) {
const formattedContent = formatter(value);
@@ -67,9 +80,21 @@ export default {
: templater.unwrap(formattedContent);
return templatedContent;
},
- onInputChange(newVal) {
- this.parsedSource.sync(newVal, this.isWysiwygMode);
+ refreshEditHelpers() {
this.isModified = this.parsedSource.isModified();
+ this.hasMatter = this.parsedSource.hasMatter();
+ },
+ onDrawerOpen() {
+ this.isDrawerOpen = true;
+ this.refreshEditHelpers();
+ },
+ onDrawerClose() {
+ this.isDrawerOpen = false;
+ this.refreshEditHelpers();
+ },
+ onInputChange(newVal) {
+ this.parsedSource.syncContent(newVal, this.isWysiwygMode);
+ this.refreshEditHelpers();
},
onModeChange(mode) {
this.editorMode = mode;
@@ -77,6 +102,9 @@ export default {
const preProcessedContent = this.preProcess(this.isWysiwygMode, this.editableContent);
this.$refs.editor.resetInitialValue(preProcessedContent);
},
+ onUpdateSettings(settings) {
+ this.parsedSource.syncMatter(settings);
+ },
onUploadImage({ file, imageUrl }) {
this.$options.imageRepository.add(file, imageUrl);
},
@@ -93,12 +121,19 @@ export default {
<template>
<div class="d-flex flex-grow-1 flex-column h-100">
<edit-header class="py-2" :title="title" />
+ <edit-drawer
+ v-if="hasMatter"
+ :is-open="isDrawerOpen"
+ :settings="editableMatter"
+ @close="onDrawerClose"
+ @updateSettings="onUpdateSettings"
+ />
<rich-content-editor
ref="editor"
:content="editableContent"
:initial-edit-type="editorMode"
:image-root="imageRoot"
- class="mb-9 h-100"
+ class="mb-9 pb-6 h-100"
@modeChange="onModeChange"
@input="onInputChange"
@uploadImage="onUploadImage"
@@ -106,9 +141,11 @@ export default {
<unsaved-changes-confirm-dialog :modified="isModified" />
<publish-toolbar
class="gl-fixed gl-left-0 gl-bottom-0 gl-w-full"
+ :has-settings="hasSettings"
:return-url="returnUrl"
:saveable="isModified"
:saving-changes="savingChanges"
+ @editSettings="onDrawerOpen"
@submit="onSubmit"
/>
</div>
diff --git a/app/assets/javascripts/static_site_editor/components/edit_drawer.vue b/app/assets/javascripts/static_site_editor/components/edit_drawer.vue
new file mode 100644
index 00000000000..0484d38dde0
--- /dev/null
+++ b/app/assets/javascripts/static_site_editor/components/edit_drawer.vue
@@ -0,0 +1,32 @@
+<script>
+import { GlDrawer } from '@gitlab/ui';
+import FrontMatterControls from './front_matter_controls.vue';
+
+export default {
+ components: {
+ GlDrawer,
+ FrontMatterControls,
+ },
+ props: {
+ isOpen: {
+ type: Boolean,
+ required: true,
+ },
+ settings: {
+ type: Object,
+ required: true,
+ },
+ },
+};
+</script>
+<template>
+ <gl-drawer class="gl-pt-8" :open="isOpen" @close="$emit('close')">
+ <template #header>{{ __('Page settings') }}</template>
+ <template>
+ <front-matter-controls
+ :settings="settings"
+ @updateSettings="$emit('updateSettings', $event)"
+ />
+ </template>
+ </gl-drawer>
+</template>
diff --git a/app/assets/javascripts/static_site_editor/components/front_matter_controls.vue b/app/assets/javascripts/static_site_editor/components/front_matter_controls.vue
new file mode 100644
index 00000000000..dad3907c3ff
--- /dev/null
+++ b/app/assets/javascripts/static_site_editor/components/front_matter_controls.vue
@@ -0,0 +1,57 @@
+<script>
+import { GlForm, GlFormInput, GlFormGroup } from '@gitlab/ui';
+import { humanize } from '~/lib/utils/text_utility';
+
+export default {
+ components: {
+ GlForm,
+ GlFormInput,
+ GlFormGroup,
+ },
+ props: {
+ settings: {
+ type: Object,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ editableSettings: { ...this.settings },
+ };
+ },
+ methods: {
+ getId(type, key) {
+ return `sse-front-matter-${type}-${key}`;
+ },
+ getIsSupported(val) {
+ return ['string', 'number'].includes(typeof val);
+ },
+ getLabel(str) {
+ return humanize(str);
+ },
+ onUpdate() {
+ this.$emit('updateSettings', { ...this.editableSettings });
+ },
+ },
+};
+</script>
+<template>
+ <gl-form>
+ <template v-for="(value, key) of editableSettings">
+ <gl-form-group
+ v-if="getIsSupported(value)"
+ :id="getId('form-group', key)"
+ :key="key"
+ :label="getLabel(key)"
+ :label-for="getId('control', key)"
+ >
+ <gl-form-input
+ :id="getId('control', key)"
+ v-model.lazy="editableSettings[key]"
+ type="text"
+ @input="onUpdate"
+ />
+ </gl-form-group>
+ </template>
+ </gl-form>
+</template>
diff --git a/app/assets/javascripts/static_site_editor/components/publish_toolbar.vue b/app/assets/javascripts/static_site_editor/components/publish_toolbar.vue
index 6cd2a4dd700..2d62964cb3b 100644
--- a/app/assets/javascripts/static_site_editor/components/publish_toolbar.vue
+++ b/app/assets/javascripts/static_site_editor/components/publish_toolbar.vue
@@ -6,6 +6,11 @@ export default {
GlButton,
},
props: {
+ hasSettings: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
returnUrl: {
type: String,
required: false,
@@ -31,12 +36,21 @@ export default {
s__('StaticSiteEditor|Return to site')
}}</gl-button>
<gl-button
+ v-if="hasSettings"
+ ref="settings"
+ :disabled="savingChanges"
+ @click="$emit('editSettings')"
+ >
+ {{ __('Settings') }}
+ </gl-button>
+ <gl-button
+ ref="submit"
variant="success"
:disabled="!saveable"
:loading="savingChanges"
@click="$emit('submit')"
>
- <span>{{ __('Submit Changes') }}</span>
+ {{ __('Submit changes') }}
</gl-button>
</div>
</div>
diff --git a/app/assets/javascripts/static_site_editor/services/formatter.js b/app/assets/javascripts/static_site_editor/services/formatter.js
index 92d5e8a5df8..9a5dcd307eb 100644
--- a/app/assets/javascripts/static_site_editor/services/formatter.js
+++ b/app/assets/javascripts/static_site_editor/services/formatter.js
@@ -1,3 +1,45 @@
+import { repeat } from 'lodash';
+
+const topLevelOrderedRegexp = /^\d{1,3}/;
+const nestedLineRegexp = /^\s+/;
+
+/**
+ * DISCLAIMER: This is a temporary fix that corrects the indentation
+ * spaces of list items. This workaround originates in the usage of
+ * the Static Site Editor to edit the Handbook. The Handbook uses a
+ * Markdown parser called Kramdown interprets lines indented
+ * with two spaces as content within a list. For example:
+ *
+ * 1. ordered list
+ * - nested unordered list
+ *
+ * The Static Site Editor uses a different Markdown parser based on the
+ * CommonMark specification (official Markdown spec) called ToastMark.
+ * When the SSE encounters a nested list with only two spaces, it flattens
+ * the list:
+ *
+ * 1. ordered list
+ * - nested unordered list
+ *
+ * This function attempts to correct this problem before the content is loaded
+ * by Toast UI.
+ */
+const correctNestedContentIndenation = source => {
+ const lines = source.split('\n');
+ let topLevelOrderedListDetected = false;
+
+ return lines
+ .reduce((result, line) => {
+ if (topLevelOrderedListDetected && nestedLineRegexp.test(line)) {
+ return [...result, line.replace(nestedLineRegexp, repeat(' ', 4))];
+ }
+
+ topLevelOrderedListDetected = topLevelOrderedRegexp.test(line);
+ return [...result, line];
+ }, [])
+ .join('\n');
+};
+
const removeOrphanedBrTags = source => {
/* Until the underlying Squire editor of Toast UI Editor resolves duplicate `<br>` tags, this
`replace` solution will clear out orphaned `<br>` tags that it generates. Additionally,
@@ -8,7 +50,7 @@ const removeOrphanedBrTags = source => {
};
const format = source => {
- return removeOrphanedBrTags(source);
+ return correctNestedContentIndenation(removeOrphanedBrTags(source));
};
export default format;
diff --git a/app/assets/javascripts/static_site_editor/services/image_service.js b/app/assets/javascripts/static_site_editor/services/image_service.js
index edc69d0579a..25ab1084572 100644
--- a/app/assets/javascripts/static_site_editor/services/image_service.js
+++ b/app/assets/javascripts/static_site_editor/services/image_service.js
@@ -1,4 +1,3 @@
-// eslint-disable-next-line import/prefer-default-export
export const getBinary = file => {
return new Promise((resolve, reject) => {
const reader = new FileReader();
diff --git a/app/assets/javascripts/static_site_editor/services/parse_source_file.js b/app/assets/javascripts/static_site_editor/services/parse_source_file.js
index 126dfe81b90..640186ee1d0 100644
--- a/app/assets/javascripts/static_site_editor/services/parse_source_file.js
+++ b/app/assets/javascripts/static_site_editor/services/parse_source_file.js
@@ -1,64 +1,40 @@
-const parseSourceFile = raw => {
- const frontMatterRegex = /(^---$[\s\S]*?^---$)/m;
- const preGroupedRegex = /([\s\S]*?)(^---$[\s\S]*?^---$)(\s*)([\s\S]*)/m; // preFrontMatter, frontMatter, spacing, and content
- let initial;
- let editable;
-
- const hasFrontMatter = source => frontMatterRegex.test(source);
+import grayMatter from 'gray-matter';
- const buildPayload = (source, header, spacing, body) => {
- return { raw: source, header, spacing, body };
- };
+const parseSourceFile = raw => {
+ const remake = source => grayMatter(source, {});
- const parse = source => {
- if (hasFrontMatter(source)) {
- const match = source.match(preGroupedRegex);
- const [, preFrontMatter, frontMatter, spacing, content] = match;
- const header = preFrontMatter + frontMatter;
+ let editable = remake(raw);
- return buildPayload(source, header, spacing, content);
+ const syncContent = (newVal, isBody) => {
+ if (isBody) {
+ editable.content = newVal;
+ } else {
+ editable = remake(newVal);
}
-
- return buildPayload(source, '', '', source);
};
- const syncEditable = () => {
- /*
- We re-parse as markdown editing could have added non-body changes (preFrontMatter, frontMatter, or spacing).
- Re-parsing additionally gets us the desired body that was extracted from the potentially mutated editable.raw
- */
- editable = parse(editable.raw);
- };
+ const trimmedEditable = () => grayMatter.stringify(editable).trim();
- const syncBodyToRaw = () => {
- editable.raw = `${editable.header}${editable.spacing}${editable.body}`;
- };
-
- const sync = (newVal, isBodyToRaw) => {
- const editableKey = isBodyToRaw ? 'body' : 'raw';
- editable[editableKey] = newVal;
+ const content = (isBody = false) => (isBody ? editable.content.trim() : trimmedEditable()); // gray-matter internally adds an eof newline so we trim to bypass, open issue: https://github.com/jonschlinkert/gray-matter/issues/96
- if (isBodyToRaw) {
- syncBodyToRaw();
- }
-
- syncEditable();
- };
+ const matter = () => editable.data;
- const content = (isBody = false) => {
- const editableKey = isBody ? 'body' : 'raw';
- return editable[editableKey];
+ const syncMatter = settings => {
+ const source = grayMatter.stringify(editable.content, settings);
+ syncContent(source);
};
- const isModified = () => initial.raw !== editable.raw;
+ const isModified = () => trimmedEditable() !== raw;
- initial = parse(raw);
- editable = parse(raw);
+ const hasMatter = () => editable.matter.length > 0;
return {
+ matter,
+ syncMatter,
content,
+ syncContent,
isModified,
- sync,
+ hasMatter,
};
};