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:
-rw-r--r--.gitlab/ci/rules.gitlab-ci.yml6
-rw-r--r--.gitlab/ci/vendored-gems.gitlab-ci.yml7
-rw-r--r--Gemfile2
-rw-r--r--Gemfile.lock12
-rw-r--r--app/assets/javascripts/issues/show/components/description.vue116
-rw-r--r--app/assets/javascripts/work_items/components/item_state.vue2
-rw-r--r--app/assets/javascripts/work_items/components/item_title.vue2
-rw-r--r--app/assets/javascripts/work_items/components/work_item_assignees.vue12
-rw-r--r--app/assets/javascripts/work_items/components/work_item_description.vue70
-rw-r--r--app/assets/javascripts/work_items/components/work_item_detail.vue16
-rw-r--r--app/assets/javascripts/work_items/components/work_item_detail_modal.vue1
-rw-r--r--app/assets/javascripts/work_items/components/work_item_weight.vue8
-rw-r--r--app/assets/javascripts/work_items/constants.js2
-rw-r--r--app/assets/javascripts/work_items/graphql/create_work_item_from_task.mutation.graphql8
-rw-r--r--app/assets/javascripts/work_items/pages/create_work_item.vue35
-rw-r--r--app/models/users/group_callout.rb3
-rw-r--r--config/feature_flags/development/blame_page_pagination.yml2
-rw-r--r--doc/administration/audit_event_streaming.md4
-rw-r--r--doc/api/graphql/reference/index.md2
-rw-r--r--doc/user/tasks.md3
-rw-r--r--locale/gitlab.pot8
-rw-r--r--spec/frontend/issues/show/components/description_spec.js43
-rw-r--r--spec/frontend/work_items/mock_data.js36
-rw-r--r--spec/frontend/work_items/pages/create_work_item_spec.js52
-rw-r--r--vendor/gems/ipynbdiff/.gitignore2
-rw-r--r--vendor/gems/ipynbdiff/.gitlab-ci.yml32
-rw-r--r--vendor/gems/ipynbdiff/Gemfile5
-rw-r--r--vendor/gems/ipynbdiff/Gemfile.lock64
-rw-r--r--vendor/gems/ipynbdiff/LICENSE21
-rw-r--r--vendor/gems/ipynbdiff/README.md56
-rw-r--r--vendor/gems/ipynbdiff/ipynbdiff.gemspec34
-rw-r--r--vendor/gems/ipynbdiff/lib/diff.rb20
-rw-r--r--vendor/gems/ipynbdiff/lib/ipynb_symbol_map.rb218
-rw-r--r--vendor/gems/ipynbdiff/lib/ipynbdiff.rb23
-rw-r--r--vendor/gems/ipynbdiff/lib/output_transformer.rb83
-rw-r--r--vendor/gems/ipynbdiff/lib/symbolized_markdown_helper.rb26
-rw-r--r--vendor/gems/ipynbdiff/lib/transformed_notebook.rb20
-rw-r--r--vendor/gems/ipynbdiff/lib/transformer.rb101
-rw-r--r--vendor/gems/ipynbdiff/lib/version.rb5
-rw-r--r--vendor/gems/ipynbdiff/spec/ipynb_symbol_map_spec.rb165
-rw-r--r--vendor/gems/ipynbdiff/spec/ipynbdiff_spec.rb126
-rw-r--r--vendor/gems/ipynbdiff/spec/testdata/backslash_as_last_char/expected.md7
-rw-r--r--vendor/gems/ipynbdiff/spec/testdata/backslash_as_last_char/expected_symbols.txt7
-rw-r--r--vendor/gems/ipynbdiff/spec/testdata/backslash_as_last_char/input.ipynb16
-rw-r--r--vendor/gems/ipynbdiff/spec/testdata/error_output/expected.md16
-rw-r--r--vendor/gems/ipynbdiff/spec/testdata/error_output/expected_symbols.txt16
-rw-r--r--vendor/gems/ipynbdiff/spec/testdata/error_output/input.ipynb32
-rw-r--r--vendor/gems/ipynbdiff/spec/testdata/from.ipynb198
-rw-r--r--vendor/gems/ipynbdiff/spec/testdata/hide_images/expected.md12
-rw-r--r--vendor/gems/ipynbdiff/spec/testdata/hide_images/expected_symbols.txt12
-rw-r--r--vendor/gems/ipynbdiff/spec/testdata/hide_images/input.ipynb45
-rw-r--r--vendor/gems/ipynbdiff/spec/testdata/ignore_html_output/expected.md11
-rw-r--r--vendor/gems/ipynbdiff/spec/testdata/ignore_html_output/expected_symbols.txt11
-rw-r--r--vendor/gems/ipynbdiff/spec/testdata/ignore_html_output/input.ipynb74
-rw-r--r--vendor/gems/ipynbdiff/spec/testdata/latex_output/expected.md10
-rw-r--r--vendor/gems/ipynbdiff/spec/testdata/latex_output/expected_symbols.txt10
-rw-r--r--vendor/gems/ipynbdiff/spec/testdata/latex_output/input.ipynb34
-rw-r--r--vendor/gems/ipynbdiff/spec/testdata/multiline_png_output/expected.md9
-rw-r--r--vendor/gems/ipynbdiff/spec/testdata/multiline_png_output/expected_symbols.txt9
-rw-r--r--vendor/gems/ipynbdiff/spec/testdata/multiline_png_output/input.ipynb25
-rw-r--r--vendor/gems/ipynbdiff/spec/testdata/no_cells/expected.md19
-rw-r--r--vendor/gems/ipynbdiff/spec/testdata/no_cells/expected_symbols.txt19
-rw-r--r--vendor/gems/ipynbdiff/spec/testdata/no_cells/input.ipynb25
-rw-r--r--vendor/gems/ipynbdiff/spec/testdata/no_cells_no_metadata/expected.md0
-rw-r--r--vendor/gems/ipynbdiff/spec/testdata/no_cells_no_metadata/expected_symbols.txt0
-rw-r--r--vendor/gems/ipynbdiff/spec/testdata/no_cells_no_metadata/input.ipynb25
-rw-r--r--vendor/gems/ipynbdiff/spec/testdata/no_metadata_on_cell/expected.md13
-rw-r--r--vendor/gems/ipynbdiff/spec/testdata/no_metadata_on_cell/expected_symbols.txt13
-rw-r--r--vendor/gems/ipynbdiff/spec/testdata/no_metadata_on_cell/input.ipynb29
-rw-r--r--vendor/gems/ipynbdiff/spec/testdata/only_code/expected.md7
-rw-r--r--vendor/gems/ipynbdiff/spec/testdata/only_code/expected_symbols.txt7
-rw-r--r--vendor/gems/ipynbdiff/spec/testdata/only_code/input.ipynb21
-rw-r--r--vendor/gems/ipynbdiff/spec/testdata/only_code_no_kernelspec/expected.md5
-rw-r--r--vendor/gems/ipynbdiff/spec/testdata/only_code_no_kernelspec/expected_symbols.txt5
-rw-r--r--vendor/gems/ipynbdiff/spec/testdata/only_code_no_kernelspec/input.ipynb12
-rw-r--r--vendor/gems/ipynbdiff/spec/testdata/only_code_no_language/expected.md5
-rw-r--r--vendor/gems/ipynbdiff/spec/testdata/only_code_no_language/expected_symbols.txt5
-rw-r--r--vendor/gems/ipynbdiff/spec/testdata/only_code_no_language/input.ipynb14
-rw-r--r--vendor/gems/ipynbdiff/spec/testdata/only_code_no_metadata/expected.md5
-rw-r--r--vendor/gems/ipynbdiff/spec/testdata/only_code_no_metadata/expected_symbols.txt5
-rw-r--r--vendor/gems/ipynbdiff/spec/testdata/only_code_no_metadata/input.ipynb11
-rw-r--r--vendor/gems/ipynbdiff/spec/testdata/only_md/expected.md5
-rw-r--r--vendor/gems/ipynbdiff/spec/testdata/only_md/expected_symbols.txt5
-rw-r--r--vendor/gems/ipynbdiff/spec/testdata/only_md/input.ipynb21
-rw-r--r--vendor/gems/ipynbdiff/spec/testdata/only_raw/expected.md4
-rw-r--r--vendor/gems/ipynbdiff/spec/testdata/only_raw/expected_symbols.txt4
-rw-r--r--vendor/gems/ipynbdiff/spec/testdata/only_raw/input.ipynb15
-rw-r--r--vendor/gems/ipynbdiff/spec/testdata/percent_decorator/expected.md70
-rw-r--r--vendor/gems/ipynbdiff/spec/testdata/percent_decorator/expected_symbols.txt70
-rw-r--r--vendor/gems/ipynbdiff/spec/testdata/single_line_md/expected.md3
-rw-r--r--vendor/gems/ipynbdiff/spec/testdata/single_line_md/expected_symbols.txt3
-rw-r--r--vendor/gems/ipynbdiff/spec/testdata/single_line_md/input.ipynb17
-rw-r--r--vendor/gems/ipynbdiff/spec/testdata/stream_text/expected.md9
-rw-r--r--vendor/gems/ipynbdiff/spec/testdata/stream_text/expected_symbols.txt9
-rw-r--r--vendor/gems/ipynbdiff/spec/testdata/stream_text/input.ipynb27
-rw-r--r--vendor/gems/ipynbdiff/spec/testdata/svg/expected.md19
-rw-r--r--vendor/gems/ipynbdiff/spec/testdata/svg/expected_symbols.txt19
-rw-r--r--vendor/gems/ipynbdiff/spec/testdata/svg/input.ipynb66
-rw-r--r--vendor/gems/ipynbdiff/spec/testdata/text_output/expected.md9
-rw-r--r--vendor/gems/ipynbdiff/spec/testdata/text_output/expected_symbols.txt9
-rw-r--r--vendor/gems/ipynbdiff/spec/testdata/text_output/input.ipynb31
-rw-r--r--vendor/gems/ipynbdiff/spec/testdata/text_png_output/expected.md16
-rw-r--r--vendor/gems/ipynbdiff/spec/testdata/text_png_output/expected_symbols.txt16
-rw-r--r--vendor/gems/ipynbdiff/spec/testdata/text_png_output/input.ipynb49
-rw-r--r--vendor/gems/ipynbdiff/spec/testdata/to.ipynb200
-rw-r--r--vendor/gems/ipynbdiff/spec/testdata/unknown_output_type/expected.md5
-rw-r--r--vendor/gems/ipynbdiff/spec/testdata/unknown_output_type/expected_symbols.txt5
-rw-r--r--vendor/gems/ipynbdiff/spec/testdata/unknown_output_type/input.ipynb27
-rw-r--r--vendor/gems/ipynbdiff/spec/transformer_spec.rb90
109 files changed, 2869 insertions, 206 deletions
diff --git a/.gitlab/ci/rules.gitlab-ci.yml b/.gitlab/ci/rules.gitlab-ci.yml
index 6b2cc01bfb8..4b7f60c1bac 100644
--- a/.gitlab/ci/rules.gitlab-ci.yml
+++ b/.gitlab/ci/rules.gitlab-ci.yml
@@ -1495,6 +1495,12 @@
changes: ["vendor/gems/mail-smtp_pool/**/*"]
- <<: *if-merge-request-labels-run-all-rspec
+.vendor:rules:ipynbdiff:
+ rules:
+ - <<: *if-merge-request
+ changes: ["vendor/gems/ipynbdiff/**/*"]
+ - <<: *if-merge-request-labels-run-all-rspec
+
##################
# Releases rules #
##################
diff --git a/.gitlab/ci/vendored-gems.gitlab-ci.yml b/.gitlab/ci/vendored-gems.gitlab-ci.yml
index a39c4307c13..ce71820100f 100644
--- a/.gitlab/ci/vendored-gems.gitlab-ci.yml
+++ b/.gitlab/ci/vendored-gems.gitlab-ci.yml
@@ -5,3 +5,10 @@ vendor mail-smtp_pool:
trigger:
include: vendor/gems/mail-smtp_pool/.gitlab-ci.yml
strategy: depend
+vendor ipynbdiff:
+ extends:
+ - .vendor:rules:ipynbdiff
+ needs: []
+ trigger:
+ include: vendor/gems/ipynbdiff/.gitlab-ci.yml
+ strategy: depend
diff --git a/Gemfile b/Gemfile
index f88cc337a13..b07f7450121 100644
--- a/Gemfile
+++ b/Gemfile
@@ -546,6 +546,6 @@ gem 'ipaddress', '~> 0.8.3'
gem 'parslet', '~> 1.8'
-gem 'ipynbdiff', '0.4.7'
+gem 'ipynbdiff', path: 'vendor/gems/ipynbdiff'
gem 'ed25519', '~> 1.3.0'
diff --git a/Gemfile.lock b/Gemfile.lock
index bd08985692f..a4d63782aac 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -1,4 +1,11 @@
PATH
+ remote: vendor/gems/ipynbdiff
+ specs:
+ ipynbdiff (0.4.7)
+ diffy (~> 3.3)
+ json (~> 2.5, >= 2.5.1)
+
+PATH
remote: vendor/gems/mail-smtp_pool
specs:
mail-smtp_pool (0.1.0)
@@ -667,9 +674,6 @@ GEM
invisible_captcha (1.1.0)
rails (>= 4.2)
ipaddress (0.8.3)
- ipynbdiff (0.4.7)
- diffy (~> 3.3)
- json (~> 2.5, >= 2.5.1)
jaeger-client (1.1.0)
opentracing (~> 0.3)
thrift
@@ -1575,7 +1579,7 @@ DEPENDENCIES
icalendar
invisible_captcha (~> 1.1.0)
ipaddress (~> 0.8.3)
- ipynbdiff (= 0.4.7)
+ ipynbdiff!
jira-ruby (~> 2.1.4)
js_regex (~> 3.7)
json (~> 2.5.1)
diff --git a/app/assets/javascripts/issues/show/components/description.vue b/app/assets/javascripts/issues/show/components/description.vue
index 892c631f8ea..7dd9dc4a01f 100644
--- a/app/assets/javascripts/issues/show/components/description.vue
+++ b/app/assets/javascripts/issues/show/components/description.vue
@@ -1,11 +1,5 @@
<script>
-import {
- GlSafeHtmlDirective as SafeHtml,
- GlModal,
- GlToast,
- GlTooltip,
- GlModalDirective,
-} from '@gitlab/ui';
+import { GlSafeHtmlDirective as SafeHtml, GlToast, GlTooltip, GlModalDirective } from '@gitlab/ui';
import $ from 'jquery';
import Sortable from 'sortablejs';
import Vue from 'vue';
@@ -20,11 +14,16 @@ import { getSortableDefaultOptions, isDragging } from '~/sortable/utils';
import TaskList from '~/task_list';
import Tracking from '~/tracking';
import workItemQuery from '~/work_items/graphql/work_item.query.graphql';
+import projectWorkItemTypesQuery from '~/work_items/graphql/project_work_item_types.query.graphql';
+import createWorkItemFromTaskMutation from '~/work_items/graphql/create_work_item_from_task.mutation.graphql';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import WorkItemDetailModal from '~/work_items/components/work_item_detail_modal.vue';
-import { TRACKING_CATEGORY_SHOW } from '~/work_items/constants';
-import CreateWorkItem from '~/work_items/pages/create_work_item.vue';
+import {
+ TRACKING_CATEGORY_SHOW,
+ TASK_TYPE_NAME,
+ WIDGET_TYPE_DESCRIPTION,
+} from '~/work_items/constants';
import animateMixin from '../mixins/animate';
import { convertDescriptionWithNewSort } from '../utils';
@@ -40,12 +39,11 @@ export default {
GlModal: GlModalDirective,
},
components: {
- GlModal,
- CreateWorkItem,
GlTooltip,
WorkItemDetailModal,
},
mixins: [animateMixin, glFeatureFlagMixin(), Tracking.mixin()],
+ inject: ['fullPath'],
props: {
canUpdate: {
type: Boolean,
@@ -103,6 +101,7 @@ export default {
workItemId: isPositiveInteger(workItemId)
? convertToGraphQLId(TYPE_WORK_ITEM, workItemId)
: undefined,
+ workItemTypes: [],
};
},
apollo: {
@@ -117,11 +116,28 @@ export default {
return !this.workItemId || !this.workItemsEnabled;
},
},
+ workItemTypes: {
+ query: projectWorkItemTypesQuery,
+ variables() {
+ return {
+ fullPath: this.fullPath,
+ };
+ },
+ update(data) {
+ return data.workspace?.workItemTypes?.nodes;
+ },
+ skip() {
+ return !this.workItemsEnabled;
+ },
+ },
},
computed: {
workItemsEnabled() {
return this.glFeatures.workItems;
},
+ taskWorkItemType() {
+ return this.workItemTypes.find((type) => type.name === TASK_TYPE_NAME)?.id;
+ },
issueGid() {
return this.issueId ? convertToGraphQLId(TYPE_WORK_ITEM, this.issueId) : null;
},
@@ -344,8 +360,8 @@ export default {
<use href="${gon.sprite_icons}#doc-new"></use>
</svg>
`;
- button.setAttribute('aria-label', s__('WorkItem|Convert to work item'));
- button.addEventListener('click', () => this.openCreateTaskModal(button));
+ button.setAttribute('aria-label', s__('WorkItem|Create task'));
+ button.addEventListener('click', () => this.handleCreateTask(button));
this.insertButtonNextToTaskText(item, button);
});
},
@@ -386,17 +402,11 @@ export default {
lineNumberEnd: lineNumbers[1],
};
},
- openCreateTaskModal(el) {
- this.setActiveTask(el);
- this.$refs.modal.show();
- },
- closeCreateTaskModal() {
- this.$refs.modal.hide();
- },
openWorkItemDetailModal(el) {
if (!el) {
return;
}
+
this.setActiveTask(el);
this.$refs.detailsModal.show();
},
@@ -404,9 +414,54 @@ export default {
this.workItemId = undefined;
this.updateWorkItemIdUrlQuery(undefined);
},
- handleCreateTask(description) {
- this.$emit('updateDescription', description);
- this.closeCreateTaskModal();
+ async handleCreateTask(el) {
+ this.setActiveTask(el);
+ try {
+ const { data } = await this.$apollo.mutate({
+ mutation: createWorkItemFromTaskMutation,
+ variables: {
+ input: {
+ id: this.issueGid,
+ workItemData: {
+ lockVersion: this.lockVersion,
+ title: this.activeTask.title,
+ lineNumberStart: Number(this.activeTask.lineNumberStart),
+ lineNumberEnd: Number(this.activeTask.lineNumberEnd),
+ workItemTypeId: this.taskWorkItemType,
+ },
+ },
+ },
+ update(store, { data: { workItemCreateFromTask } }) {
+ const { newWorkItem } = workItemCreateFromTask;
+
+ store.writeQuery({
+ query: workItemQuery,
+ variables: {
+ id: newWorkItem.id,
+ },
+ data: {
+ workItem: newWorkItem,
+ },
+ });
+ },
+ });
+
+ const { workItem, newWorkItem } = data.workItemCreateFromTask;
+
+ const updatedDescription = workItem?.widgets?.find(
+ (widget) => widget.type === WIDGET_TYPE_DESCRIPTION,
+ )?.descriptionHtml;
+
+ this.$emit('updateDescription', updatedDescription);
+ this.workItemId = newWorkItem.id;
+ this.openWorkItemDetailModal(el);
+ } catch (error) {
+ createFlash({
+ message: s__('WorkItem|Something went wrong when creating a work item. Please try again'),
+ error,
+ captureError: true,
+ });
+ }
},
handleDeleteTask(description) {
this.$emit('updateDescription', description);
@@ -452,19 +507,6 @@ export default {
data-testid="textarea"
>
</textarea>
-
- <gl-modal ref="modal" size="lg" modal-id="create-task-modal" hide-footer body-class="gl-p-0!">
- <create-work-item
- is-modal
- :initial-title="activeTask.title"
- :issue-gid="issueGid"
- :lock-version="lockVersion"
- :line-number-start="activeTask.lineNumberStart"
- :line-number-end="activeTask.lineNumberEnd"
- @closeModal="closeCreateTaskModal"
- @onCreate="handleCreateTask"
- />
- </gl-modal>
<work-item-detail-modal
ref="detailsModal"
:can-update="canUpdate"
@@ -478,7 +520,7 @@ export default {
/>
<template v-if="workItemsEnabled">
<gl-tooltip v-for="item in taskButtons" :key="item" :target="item">
- {{ s__('WorkItem|Convert to work item') }}
+ {{ s__('WorkItem|Create task') }}
</gl-tooltip>
</template>
</div>
diff --git a/app/assets/javascripts/work_items/components/item_state.vue b/app/assets/javascripts/work_items/components/item_state.vue
index 69670d3471c..2dc8e3a1101 100644
--- a/app/assets/javascripts/work_items/components/item_state.vue
+++ b/app/assets/javascripts/work_items/components/item_state.vue
@@ -54,7 +54,7 @@ export default {
:label-for="$options.labelId"
label-cols="3"
label-cols-lg="2"
- label-class="gl-pb-0!"
+ label-class="gl-pb-0! gl-overflow-wrap-break"
class="gl-align-items-center"
>
<gl-form-select
diff --git a/app/assets/javascripts/work_items/components/item_title.vue b/app/assets/javascripts/work_items/components/item_title.vue
index ce2fa158596..19fbad4eaa3 100644
--- a/app/assets/javascripts/work_items/components/item_title.vue
+++ b/app/assets/javascripts/work_items/components/item_title.vue
@@ -40,7 +40,7 @@ export default {
<template>
<h2
- class="gl-font-weight-normal gl-sm-font-weight-bold gl-my-5 gl-w-full"
+ class="gl-font-weight-normal gl-sm-font-weight-bold gl-mb-5 gl-mt-0 gl-w-full"
:class="{ 'gl-cursor-not-allowed': disabled }"
aria-labelledby="item-title"
>
diff --git a/app/assets/javascripts/work_items/components/work_item_assignees.vue b/app/assets/javascripts/work_items/components/work_item_assignees.vue
index 46920969415..45add8c8d74 100644
--- a/app/assets/javascripts/work_items/components/work_item_assignees.vue
+++ b/app/assets/javascripts/work_items/components/work_item_assignees.vue
@@ -137,17 +137,19 @@ export default {
</script>
<template>
- <div class="gl-display-flex gl-mb-4 work-item-assignees gl-relative">
- <span class="gl-font-weight-bold gl-w-15 gl-pt-2" data-testid="assignees-title">{{
- assigneeText
- }}</span>
+ <div class="form-row gl-mb-5 work-item-assignees gl-relative">
+ <span
+ class="gl-font-weight-bold col-lg-2 col-3 gl-pt-2 min-w-fit-content gl-overflow-wrap-break"
+ data-testid="assignees-title"
+ >{{ assigneeText }}</span
+ >
<gl-token-selector
ref="tokenSelector"
v-model="localAssignees"
:container-class="containerClass"
+ class="gl-flex-grow-1 gl-border gl-border-white gl-hover-border-gray-200 gl-rounded-base col-9 gl-align-self-start"
:dropdown-items="searchUsers"
:loading="isLoading"
- class="gl-w-full gl-border gl-border-white gl-hover-border-gray-200 gl-rounded-base"
@input="focusTokenSelector"
@text-input="debouncedSearchKeyUpdate"
@focus="handleFocus"
diff --git a/app/assets/javascripts/work_items/components/work_item_description.vue b/app/assets/javascripts/work_items/components/work_item_description.vue
index 5a85fcdd7ac..90e3cd45cb4 100644
--- a/app/assets/javascripts/work_items/components/work_item_description.vue
+++ b/app/assets/javascripts/work_items/components/work_item_description.vue
@@ -35,7 +35,7 @@ export default {
isEditing: false,
isSubmitting: false,
isSubmittingWithKeydown: false,
- desc: '',
+ descriptionText: '',
};
},
apollo: {
@@ -71,16 +71,17 @@ export default {
descriptionHtml() {
return this.workItemDescription?.descriptionHtml;
},
- descriptionText: {
- get() {
- return this.desc;
- },
- set(desc) {
- this.desc = desc;
- },
+ descriptionEmpty() {
+ return this.descriptionHtml?.trim() === '';
},
workItemDescription() {
- return this.workItem?.widgets?.find((widget) => widget.type === WIDGET_TYPE_DESCRIPTION);
+ const descriptionWidget = this.workItem?.widgets?.find(
+ (widget) => widget.type === WIDGET_TYPE_DESCRIPTION,
+ );
+ return {
+ ...descriptionWidget,
+ description: descriptionWidget?.description || '',
+ };
},
workItemType() {
return this.workItem?.workItemType?.name;
@@ -95,14 +96,14 @@ export default {
async startEditing() {
this.isEditing = true;
- this.desc = getDraft(this.autosaveKey) || this.workItemDescription?.description || '';
+ this.descriptionText = getDraft(this.autosaveKey) || this.workItemDescription?.description;
await this.$nextTick();
this.$refs.textarea.focus();
},
async cancelEditing() {
- const isDirty = this.desc !== this.workItemDescription?.description;
+ const isDirty = this.descriptionText !== this.workItemDescription?.description;
if (isDirty) {
const msg = s__('WorkItem|Are you sure you want to cancel editing?');
@@ -125,7 +126,7 @@ export default {
return;
}
- updateDraft(this.autosaveKey, this.desc);
+ updateDraft(this.autosaveKey, this.descriptionText);
},
async updateWorkItem(event) {
if (event.key) {
@@ -171,25 +172,10 @@ export default {
<template>
<gl-form-group
v-if="isEditing"
- class="gl-pt-5 gl-mb-5 gl-mt-0! gl-border-t! gl-border-b"
+ class="gl-my-5"
:label="__('Description')"
label-for="work-item-description"
- label-class="gl-float-left"
>
- <div class="gl-display-flex gl-justify-content-flex-end">
- <gl-button class="gl-ml-auto" data-testid="cancel" @click="cancelEditing">{{
- __('Cancel')
- }}</gl-button>
- <gl-button
- class="js-no-auto-disable gl-ml-4"
- category="primary"
- variant="confirm"
- :loading="isSubmitting"
- data-testid="save-description"
- @click="updateWorkItem"
- >{{ __('Save') }}</gl-button
- >
- </div>
<markdown-field
can-attach-file
:textarea-value="descriptionText"
@@ -216,19 +202,35 @@ export default {
></textarea>
</template>
</markdown-field>
- </gl-form-group>
- <div v-else class="gl-pt-5 gl-mb-5 gl-border-t gl-border-b">
+
<div class="gl-display-flex">
- <h3 class="gl-font-base gl-mt-0">{{ __('Description') }}</h3>
+ <gl-button
+ category="primary"
+ variant="confirm"
+ :loading="isSubmitting"
+ data-testid="save-description"
+ @click="updateWorkItem"
+ >{{ __('Save') }}</gl-button
+ >
+ <gl-button category="tertiary" class="gl-ml-3" data-testid="cancel" @click="cancelEditing">{{
+ __('Cancel')
+ }}</gl-button>
+ </div>
+ </gl-form-group>
+ <div v-else class="gl-mb-5">
+ <div class="gl-display-flex gl-align-items-center gl-mb-5">
+ <h3 class="gl-font-base gl-my-0">{{ __('Description') }}</h3>
<gl-button
v-if="canEdit"
class="gl-ml-auto"
icon="pencil"
data-testid="edit-description"
+ :aria-label="__('Edit')"
@click="startEditing"
- >{{ __('Edit') }}</gl-button
- >
+ />
</div>
- <div v-safe-html="descriptionHtml" class="md gl-mb-5"></div>
+
+ <div v-if="descriptionEmpty" class="gl-text-secondary gl-mb-5">{{ __('None') }}</div>
+ <div v-else v-safe-html="descriptionHtml" class="md gl-mb-5 gl-min-h-8"></div>
</div>
</template>
diff --git a/app/assets/javascripts/work_items/components/work_item_detail.vue b/app/assets/javascripts/work_items/components/work_item_detail.vue
index 5272df2d53f..606d65a08f8 100644
--- a/app/assets/javascripts/work_items/components/work_item_detail.vue
+++ b/app/assets/javascripts/work_items/components/work_item_detail.vue
@@ -101,7 +101,7 @@ export default {
</script>
<template>
- <section>
+ <section class="gl-pt-5">
<gl-alert v-if="error" variant="danger" @dismiss="error = undefined">
{{ error }}
</gl-alert>
@@ -113,6 +113,7 @@ export default {
</gl-skeleton-loader>
</div>
<template v-else>
+ <div class="gl-font-weight-bold gl-text-secondary gl-mb-2">{{ workItemType }}</div>
<div class="gl-display-flex gl-align-items-start">
<work-item-title
:work-item-id="workItem.id"
@@ -125,11 +126,16 @@ export default {
<work-item-actions
:work-item-id="workItem.id"
:can-delete="canDelete"
- class="gl-ml-auto gl-mt-6"
+ class="gl-mt-4"
@deleteWorkItem="$emit('deleteWorkItem')"
@error="error = $event"
/>
</div>
+ <work-item-state
+ :work-item="workItem"
+ :work-item-parent-id="workItemParentId"
+ @error="error = $event"
+ />
<template v-if="workItemsMvc2Enabled">
<work-item-assignees
v-if="workItemAssignees"
@@ -138,14 +144,10 @@ export default {
/>
<work-item-weight v-if="workItemWeight" :weight="workItemWeight.weight" />
</template>
- <work-item-state
- :work-item="workItem"
- :work-item-parent-id="workItemParentId"
- @error="error = $event"
- />
<work-item-description
v-if="hasDescriptionWidget"
:work-item-id="workItem.id"
+ class="gl-pt-5"
@error="error = $event"
/>
</template>
diff --git a/app/assets/javascripts/work_items/components/work_item_detail_modal.vue b/app/assets/javascripts/work_items/components/work_item_detail_modal.vue
index d1c8022ac57..50753c79741 100644
--- a/app/assets/javascripts/work_items/components/work_item_detail_modal.vue
+++ b/app/assets/javascripts/work_items/components/work_item_detail_modal.vue
@@ -104,7 +104,6 @@ export default {
size="lg"
modal-id="work-item-detail-modal"
header-class="gl-p-0 gl-pb-2!"
- body-class="gl-pb-6!"
@hide="closeModal"
>
<gl-alert v-if="error" variant="danger" @dismiss="error = false">
diff --git a/app/assets/javascripts/work_items/components/work_item_weight.vue b/app/assets/javascripts/work_items/components/work_item_weight.vue
index b0f2b3aa14a..72e678151e9 100644
--- a/app/assets/javascripts/work_items/components/work_item_weight.vue
+++ b/app/assets/javascripts/work_items/components/work_item_weight.vue
@@ -19,8 +19,10 @@ export default {
</script>
<template>
- <div v-if="hasIssueWeightsFeature" class="gl-mb-5">
- <span class="gl-display-inline-block gl-font-weight-bold gl-w-15">{{ __('Weight') }}</span>
- {{ weightText }}
+ <div v-if="hasIssueWeightsFeature" class="gl-mb-5 form-row">
+ <span class="gl-font-weight-bold col-lg-2 col-3 gl-overflow-wrap-break">{{
+ __('Weight')
+ }}</span>
+ <span class="gl-ml-5">{{ weightText }}</span>
</div>
</template>
diff --git a/app/assets/javascripts/work_items/constants.js b/app/assets/javascripts/work_items/constants.js
index 2df4978a319..d5ec1900a86 100644
--- a/app/assets/javascripts/work_items/constants.js
+++ b/app/assets/javascripts/work_items/constants.js
@@ -13,7 +13,7 @@ export const i18n = {
updateError: s__('WorkItem|Something went wrong while updating the work item. Please try again.'),
};
-export const DEFAULT_MODAL_TYPE = 'Task';
+export const TASK_TYPE_NAME = 'Task';
export const WIDGET_TYPE_ASSIGNEE = 'ASSIGNEES';
export const WIDGET_TYPE_DESCRIPTION = 'DESCRIPTION';
diff --git a/app/assets/javascripts/work_items/graphql/create_work_item_from_task.mutation.graphql b/app/assets/javascripts/work_items/graphql/create_work_item_from_task.mutation.graphql
index b25210f5c74..ccfe62cc585 100644
--- a/app/assets/javascripts/work_items/graphql/create_work_item_from_task.mutation.graphql
+++ b/app/assets/javascripts/work_items/graphql/create_work_item_from_task.mutation.graphql
@@ -1,8 +1,12 @@
+#import "./work_item.fragment.graphql"
+
mutation workItemCreateFromTask($input: WorkItemCreateFromTaskInput!) {
workItemCreateFromTask(input: $input) {
workItem {
- id
- descriptionHtml
+ ...WorkItem
+ }
+ newWorkItem {
+ ...WorkItem
}
errors
}
diff --git a/app/assets/javascripts/work_items/pages/create_work_item.vue b/app/assets/javascripts/work_items/pages/create_work_item.vue
index 04c6a61689c..0047b03108a 100644
--- a/app/assets/javascripts/work_items/pages/create_work_item.vue
+++ b/app/assets/javascripts/work_items/pages/create_work_item.vue
@@ -6,7 +6,6 @@ import workItemQuery from '../graphql/work_item.query.graphql';
import createWorkItemMutation from '../graphql/create_work_item.mutation.graphql';
import createWorkItemFromTaskMutation from '../graphql/create_work_item_from_task.mutation.graphql';
import projectWorkItemTypesQuery from '../graphql/project_work_item_types.query.graphql';
-import { DEFAULT_MODAL_TYPE } from '../constants';
import ItemTitle from '../components/item_title.vue';
@@ -24,11 +23,6 @@ export default {
},
inject: ['fullPath'],
props: {
- isModal: {
- type: Boolean,
- required: false,
- default: false,
- },
initialTitle: {
type: String,
required: false,
@@ -78,13 +72,6 @@ export default {
text: node.name,
}));
},
- result() {
- if (!this.selectedWorkItemType && this.isModal) {
- this.selectedWorkItemType = this.formOptions.find(
- (options) => options.text === DEFAULT_MODAL_TYPE,
- )?.value;
- }
- },
error() {
this.error = this.$options.fetchTypesErrorText;
},
@@ -104,11 +91,7 @@ export default {
methods: {
async createWorkItem() {
this.loading = true;
- if (this.isModal) {
- await this.createWorkItemFromTask();
- } else {
- await this.createStandaloneWorkItem();
- }
+ await this.createStandaloneWorkItem();
this.loading = false;
},
async createStandaloneWorkItem() {
@@ -174,11 +157,7 @@ export default {
this.title = title;
},
handleCancelClick() {
- if (!this.isModal) {
- this.$router.go(-1);
- return;
- }
- this.$emit('closeModal');
+ this.$router.go(-1);
},
},
};
@@ -187,7 +166,7 @@ export default {
<template>
<form @submit.prevent="createWorkItem">
<gl-alert v-if="error" variant="danger" @dismiss="error = null">{{ error }}</gl-alert>
- <div :class="{ 'gl-px-5': isModal }" data-testid="content">
+ <div data-testid="content">
<item-title :title="initialTitle" data-testid="title-input" @title-input="handleTitleInput" />
<div>
<gl-loading-icon
@@ -203,14 +182,11 @@ export default {
/>
</div>
</div>
- <div
- class="gl-bg-gray-10 gl-py-5 gl-px-6 gl-mt-4"
- :class="{ 'gl-display-flex gl-justify-content-end': isModal }"
- >
+ <div class="gl-bg-gray-10 gl-py-5 gl-px-6 gl-mt-4">
<gl-button
variant="confirm"
:disabled="isButtonDisabled"
- :class="{ 'gl-mr-3': !isModal }"
+ class="gl-mr-3"
:loading="loading"
data-testid="create-button"
type="submit"
@@ -221,7 +197,6 @@ export default {
type="button"
data-testid="cancel-button"
class="gl-order-n1"
- :class="{ 'gl-mr-3': isModal }"
@click="handleCancelClick"
>
{{ __('Cancel') }}
diff --git a/app/models/users/group_callout.rb b/app/models/users/group_callout.rb
index 373bc30889f..0ea7b8199aa 100644
--- a/app/models/users/group_callout.rb
+++ b/app/models/users/group_callout.rb
@@ -16,7 +16,8 @@ module Users
storage_enforcement_banner_third_enforcement_threshold: 5,
storage_enforcement_banner_fourth_enforcement_threshold: 6,
preview_user_over_limit_free_plan_alert: 7, # EE-only
- user_reached_limit_free_plan_alert: 8 # EE-only
+ user_reached_limit_free_plan_alert: 8, # EE-only
+ free_group_limited_alert: 9 # EE-only
}
validates :group, presence: true
diff --git a/config/feature_flags/development/blame_page_pagination.yml b/config/feature_flags/development/blame_page_pagination.yml
index 49fb1a7f605..8465b40b4c5 100644
--- a/config/feature_flags/development/blame_page_pagination.yml
+++ b/config/feature_flags/development/blame_page_pagination.yml
@@ -5,4 +5,4 @@ rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/360927
milestone: '15.0'
type: development
group: group::source code
-default_enabled: false
+default_enabled: true
diff --git a/doc/administration/audit_event_streaming.md b/doc/administration/audit_event_streaming.md
index bd67d62f384..5fc92c5fb6c 100644
--- a/doc/administration/audit_event_streaming.md
+++ b/doc/administration/audit_event_streaming.md
@@ -146,10 +146,10 @@ Destination is deleted if:
## Custom HTTP header values
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/361216) in GitLab 15.1 [with a flag](feature_flags.md) named `streaming_audit_event_headers`. Disabled by default.
-> - [Enabled on GitLab.com](https://gitlab.com/gitlab-org/gitlab/-/issues/362941) in GitLab 15.2.
+> - [Enabled on GitLab.com and self-managed](https://gitlab.com/gitlab-org/gitlab/-/issues/362941) in GitLab 15.2.
FLAG:
-On self-managed GitLab, by default this feature is not available. To make it available per group, ask an administrator to [enable the feature flag](../administration/feature_flags.md) named `streaming_audit_event_headers`.
+On self-managed GitLab, by default this feature is available. To hide the feature, ask an administrator to [disable the feature flag](../administration/feature_flags.md) named `streaming_audit_event_headers`.
On GitLab.com, this feature is available.
Each streaming destination can have up to 20 custom HTTP headers included with each streamed event.
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index 9de564449ef..336aecda991 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -11520,7 +11520,7 @@ Represents an external resource to send audit events to.
| ---- | ---- | ----------- |
| <a id="externalauditeventdestinationdestinationurl"></a>`destinationUrl` | [`String!`](#string) | External destination to send audit events to. |
| <a id="externalauditeventdestinationgroup"></a>`group` | [`Group!`](#group) | Group the destination belongs to. |
-| <a id="externalauditeventdestinationheaders"></a>`headers` | [`AuditEventStreamingHeaderConnection!`](#auditeventstreamingheaderconnection) | List of additional HTTP headers sent with each event. Available only when feature flag `streaming_audit_event_headers` is enabled. This flag is disabled by default, because the feature is experimental and is subject to change without notice. (see [Connections](#connections)) |
+| <a id="externalauditeventdestinationheaders"></a>`headers` | [`AuditEventStreamingHeaderConnection!`](#auditeventstreamingheaderconnection) | List of additional HTTP headers sent with each event. Available only when feature flag `streaming_audit_event_headers` is enabled. This flag is enabled by default. (see [Connections](#connections)) |
| <a id="externalauditeventdestinationid"></a>`id` | [`ID!`](#id) | ID of the destination. |
| <a id="externalauditeventdestinationverificationtoken"></a>`verificationToken` | [`String!`](#string) | Verification token to validate source of event. |
diff --git a/doc/user/tasks.md b/doc/user/tasks.md
index fc49661c61c..5112e1afaa8 100644
--- a/doc/user/tasks.md
+++ b/doc/user/tasks.md
@@ -32,8 +32,7 @@ to work items and adding custom work item types, visit
To create a task:
1. In an issue description, create a [task list](markdown.md#task-lists).
-1. Hover over a task item and select **Convert to work item** (**{doc-new}**).
-1. Confirm or edit the title, and select **Create work item**.
+1. Hover over a task item and select **Create task** (**{doc-new}**).
## Edit a task
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index ecea30cb802..207ec7fffe5 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -6214,6 +6214,12 @@ msgstr ""
msgid "Billing|You can begin moving members in %{namespaceName} now. A member loses access to the group when you turn off %{strongStart}In a seat%{strongEnd}. If over 5 members have %{strongStart}In a seat%{strongEnd} enabled after June 22, 2022, we'll select the 5 members who maintain access. We'll first count members that have Owner and Maintainer roles, then the most recently active members until we reach 5 members. The remaining members will get a status of Over limit and lose access to the group."
msgstr ""
+msgid "Billing|Your free group is now limited to %{free_user_limit} members"
+msgstr ""
+
+msgid "Billing|Your group recently changed to use the Free plan. Free groups are limited to %{free_user_limit} members and the remaining members will get a status of over-limit and lose access to the group. You can free up space for new members by removing those who no longer need access or toggling them to over-limit. To get an unlimited number of members, you can %{link_start}upgrade%{link_end} to a paid tier."
+msgstr ""
+
msgid "Bitbucket Server Import"
msgstr ""
@@ -43524,7 +43530,7 @@ msgstr ""
msgid "WorkItem|Collapse child items"
msgstr ""
-msgid "WorkItem|Convert to work item"
+msgid "WorkItem|Create task"
msgstr ""
msgid "WorkItem|Create work item"
diff --git a/spec/frontend/issues/show/components/description_spec.js b/spec/frontend/issues/show/components/description_spec.js
index 2cc27309e59..b8a2da4fa47 100644
--- a/spec/frontend/issues/show/components/description_spec.js
+++ b/spec/frontend/issues/show/components/description_spec.js
@@ -15,10 +15,15 @@ import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import Description from '~/issues/show/components/description.vue';
import { updateHistory } from '~/lib/utils/url_utility';
import workItemQuery from '~/work_items/graphql/work_item.query.graphql';
+import workItemTypesQuery from '~/work_items/graphql/project_work_item_types.query.graphql';
+import createWorkItemFromTaskMutation from '~/work_items/graphql/create_work_item_from_task.mutation.graphql';
import TaskList from '~/task_list';
import WorkItemDetailModal from '~/work_items/components/work_item_detail_modal.vue';
import { TRACKING_CATEGORY_SHOW } from '~/work_items/constants';
-import CreateWorkItem from '~/work_items/pages/create_work_item.vue';
+import {
+ projectWorkItemTypesQueryResponse,
+ createWorkItemFromTaskMutationResponse,
+} from 'jest/work_items/mock_data';
import {
descriptionProps as initialProps,
descriptionHtmlWithCheckboxes,
@@ -46,6 +51,10 @@ const workItemQueryResponse = {
};
const queryHandler = jest.fn().mockResolvedValue(workItemQueryResponse);
+const workItemTypesQueryHandler = jest.fn().mockResolvedValue(projectWorkItemTypesQueryResponse);
+const createWorkItemFromTaskSuccessHandler = jest
+ .fn()
+ .mockResolvedValue(createWorkItemFromTaskMutationResponse);
describe('Description component', () => {
let wrapper;
@@ -60,18 +69,24 @@ describe('Description component', () => {
const findTooltips = () => wrapper.findAllComponents(GlTooltip);
const findModal = () => wrapper.findComponent(GlModal);
- const findCreateWorkItem = () => wrapper.findComponent(CreateWorkItem);
const findWorkItemDetailModal = () => wrapper.findComponent(WorkItemDetailModal);
- function createComponent({ props = {}, provide = {} } = {}) {
+ function createComponent({ props = {}, provide } = {}) {
wrapper = shallowMountExtended(Description, {
propsData: {
issueId: 1,
...initialProps,
...props,
},
- provide,
- apolloProvider: createMockApollo([[workItemQuery, queryHandler]]),
+ provide: {
+ fullPath: 'gitlab-org/gitlab-test',
+ ...provide,
+ },
+ apolloProvider: createMockApollo([
+ [workItemQuery, queryHandler],
+ [workItemTypesQuery, workItemTypesQueryHandler],
+ [createWorkItemFromTaskMutation, createWorkItemFromTaskSuccessHandler],
+ ]),
mocks: {
$toast,
},
@@ -299,24 +314,16 @@ describe('Description component', () => {
});
it('does not show a modal by default', () => {
- expect(findModal().props('visible')).toBe(false);
+ expect(findModal().exists()).toBe(false);
});
- it('opens a modal when a button is clicked and displays correct title', async () => {
- await findConvertToTaskButton().trigger('click');
- expect(findCreateWorkItem().props('initialTitle').trim()).toBe('todo 1');
- });
+ it('emits `updateDescription` after creating new work item', async () => {
+ const newDescription = `<p>New description</p>`;
- it('closes the modal on `closeCreateTaskModal` event', async () => {
await findConvertToTaskButton().trigger('click');
- findCreateWorkItem().vm.$emit('closeModal');
- expect(hideModal).toHaveBeenCalled();
- });
- it('emits `updateDescription` on `onCreate` event', () => {
- const newDescription = `<p>New description</p>`;
- findCreateWorkItem().vm.$emit('onCreate', newDescription);
- expect(hideModal).toHaveBeenCalled();
+ await waitForPromises();
+
expect(wrapper.emitted('updateDescription')).toEqual([[newDescription]]);
});
diff --git a/spec/frontend/work_items/mock_data.js b/spec/frontend/work_items/mock_data.js
index 116bf48901d..7fe405a9aac 100644
--- a/spec/frontend/work_items/mock_data.js
+++ b/spec/frontend/work_items/mock_data.js
@@ -140,13 +140,45 @@ export const createWorkItemFromTaskMutationResponse = {
__typename: 'WorkItemCreateFromTaskPayload',
errors: [],
workItem: {
- descriptionHtml: '<p>New description</p>',
- id: 'gid://gitlab/WorkItem/13',
__typename: 'WorkItem',
+ description: 'New description',
+ id: 'gid://gitlab/WorkItem/1',
+ title: 'Updated title',
+ state: 'OPEN',
+ workItemType: {
+ __typename: 'WorkItemType',
+ id: 'gid://gitlab/WorkItems::Type/5',
+ name: 'Task',
+ },
userPermissions: {
deleteWorkItem: false,
updateWorkItem: false,
},
+ widgets: [
+ {
+ __typename: 'WorkItemWidgetDescription',
+ type: 'DESCRIPTION',
+ description: 'New description',
+ descriptionHtml: '<p>New description</p>',
+ },
+ ],
+ },
+ newWorkItem: {
+ __typename: 'WorkItem',
+ id: 'gid://gitlab/WorkItem/1000000',
+ title: 'Updated title',
+ state: 'OPEN',
+ description: '',
+ workItemType: {
+ __typename: 'WorkItemType',
+ id: 'gid://gitlab/WorkItems::Type/5',
+ name: 'Task',
+ },
+ userPermissions: {
+ deleteWorkItem: false,
+ updateWorkItem: false,
+ },
+ widgets: [],
},
},
},
diff --git a/spec/frontend/work_items/pages/create_work_item_spec.js b/spec/frontend/work_items/pages/create_work_item_spec.js
index e89477ed599..fed8be3783a 100644
--- a/spec/frontend/work_items/pages/create_work_item_spec.js
+++ b/spec/frontend/work_items/pages/create_work_item_spec.js
@@ -9,11 +9,7 @@ import ItemTitle from '~/work_items/components/item_title.vue';
import projectWorkItemTypesQuery from '~/work_items/graphql/project_work_item_types.query.graphql';
import createWorkItemMutation from '~/work_items/graphql/create_work_item.mutation.graphql';
import createWorkItemFromTaskMutation from '~/work_items/graphql/create_work_item_from_task.mutation.graphql';
-import {
- projectWorkItemTypesQueryResponse,
- createWorkItemMutationResponse,
- createWorkItemFromTaskMutationResponse,
-} from '../mock_data';
+import { projectWorkItemTypesQueryResponse, createWorkItemMutationResponse } from '../mock_data';
jest.mock('~/lib/utils/uuids', () => ({ uuids: () => ['testuuid'] }));
@@ -25,9 +21,6 @@ describe('Create work item component', () => {
const querySuccessHandler = jest.fn().mockResolvedValue(projectWorkItemTypesQueryResponse);
const createWorkItemSuccessHandler = jest.fn().mockResolvedValue(createWorkItemMutationResponse);
- const createWorkItemFromTaskSuccessHandler = jest
- .fn()
- .mockResolvedValue(createWorkItemFromTaskMutationResponse);
const errorHandler = jest.fn().mockRejectedValue('Houston, we have a problem');
const findAlert = () => wrapper.findComponent(GlAlert);
@@ -122,49 +115,6 @@ describe('Create work item component', () => {
});
});
- describe('when displayed in a modal', () => {
- beforeEach(() => {
- createComponent({
- props: {
- isModal: true,
- },
- mutationHandler: createWorkItemFromTaskSuccessHandler,
- });
- });
-
- it('emits `closeModal` event on Cancel button click', () => {
- findCancelButton().vm.$emit('click');
-
- expect(wrapper.emitted('closeModal')).toEqual([[]]);
- });
-
- it('emits `onCreate` on successful mutation', async () => {
- findTitleInput().vm.$emit('title-input', 'Test title');
-
- wrapper.find('form').trigger('submit');
- await waitForPromises();
-
- expect(wrapper.emitted('onCreate')).toEqual([['<p>New description</p>']]);
- });
-
- it('does not right margin for create button', () => {
- expect(findCreateButton().classes()).not.toContain('gl-mr-3');
- });
-
- it('adds right margin for cancel button', () => {
- expect(findCancelButton().classes()).toContain('gl-mr-3');
- });
-
- it('adds padding for content', () => {
- expect(findContent().classes('gl-px-5')).toBe(true);
- });
-
- it('defaults type to `Task`', async () => {
- await waitForPromises();
- expect(findSelect().attributes('value')).toBe('gid://gitlab/WorkItems::Type/3');
- });
- });
-
it('displays a loading icon inside dropdown when work items query is loading', () => {
createComponent();
diff --git a/vendor/gems/ipynbdiff/.gitignore b/vendor/gems/ipynbdiff/.gitignore
new file mode 100644
index 00000000000..4f284c35a42
--- /dev/null
+++ b/vendor/gems/ipynbdiff/.gitignore
@@ -0,0 +1,2 @@
+*.gem
+.bundle
diff --git a/vendor/gems/ipynbdiff/.gitlab-ci.yml b/vendor/gems/ipynbdiff/.gitlab-ci.yml
new file mode 100644
index 00000000000..7b0c9df6cd9
--- /dev/null
+++ b/vendor/gems/ipynbdiff/.gitlab-ci.yml
@@ -0,0 +1,32 @@
+# You can override the included template(s) by including variable overrides
+# SAST customization: https://docs.gitlab.com/ee/user/application_security/sast/#customizing-the-sast-settings
+# Secret Detection customization: https://docs.gitlab.com/ee/user/application_security/secret_detection/#customizing-settings
+# Dependency Scanning customization: https://docs.gitlab.com/ee/user/application_security/dependency_scanning/#customizing-the-dependency-scanning-settings
+# Note that environment variables can be set in several places
+# See https://docs.gitlab.com/ee/ci/variables/#cicd-variable-precedence
+workflow:
+ rules:
+ - if: $CI_MERGE_REQUEST_ID
+
+.rspec:
+ cache:
+ key: ipynbdiff
+ paths:
+ - vendor/gems/ipynbdiff/vendor/ruby
+ before_script:
+ - cd vendor/gems/ipynbdiff
+ - ruby -v # Print out ruby version for debugging
+ - gem install bundler --no-document # Bundler is not installed with the image
+ - bundle config set --local path 'vendor' # Install dependencies into ./vendor/ruby
+ - bundle config set with 'development'
+ - bundle install -j $(nproc)
+ script:
+ - bundle exec rspec
+
+rspec-2.7:
+ image: "ruby:2.7"
+ extends: .rspec
+
+rspec-3.0:
+ image: "ruby:3.0"
+ extends: .rspec
diff --git a/vendor/gems/ipynbdiff/Gemfile b/vendor/gems/ipynbdiff/Gemfile
new file mode 100644
index 00000000000..7f4f5e950d1
--- /dev/null
+++ b/vendor/gems/ipynbdiff/Gemfile
@@ -0,0 +1,5 @@
+# frozen_string_literal: true
+
+source 'https://rubygems.org'
+
+gemspec
diff --git a/vendor/gems/ipynbdiff/Gemfile.lock b/vendor/gems/ipynbdiff/Gemfile.lock
new file mode 100644
index 00000000000..a5e8e3e4e86
--- /dev/null
+++ b/vendor/gems/ipynbdiff/Gemfile.lock
@@ -0,0 +1,64 @@
+PATH
+ remote: .
+ specs:
+ ipynbdiff (0.4.7)
+ diffy (~> 3.3)
+ json (~> 2.5, >= 2.5.1)
+
+GEM
+ remote: https://rubygems.org/
+ specs:
+ ast (2.4.2)
+ binding_ninja (0.2.3)
+ coderay (1.1.3)
+ diff-lcs (1.5.0)
+ diffy (3.4.2)
+ json (2.6.2)
+ method_source (1.0.0)
+ parser (3.1.2.0)
+ ast (~> 2.4.1)
+ proc_to_ast (0.1.0)
+ coderay
+ parser
+ unparser
+ pry (0.14.1)
+ coderay (~> 1.1)
+ method_source (~> 1.0)
+ rake (13.0.6)
+ rspec (3.11.0)
+ rspec-core (~> 3.11.0)
+ rspec-expectations (~> 3.11.0)
+ rspec-mocks (~> 3.11.0)
+ rspec-core (3.11.0)
+ rspec-support (~> 3.11.0)
+ rspec-expectations (3.11.0)
+ diff-lcs (>= 1.2.0, < 2.0)
+ rspec-support (~> 3.11.0)
+ rspec-mocks (3.11.1)
+ diff-lcs (>= 1.2.0, < 2.0)
+ rspec-support (~> 3.11.0)
+ rspec-parameterized (0.5.1)
+ binding_ninja (>= 0.2.3)
+ parser
+ proc_to_ast
+ rspec (>= 2.13, < 4)
+ unparser
+ rspec-support (3.11.0)
+ unparser (0.6.5)
+ diff-lcs (~> 1.3)
+ parser (>= 3.1.0)
+
+PLATFORMS
+ x86_64-darwin-20
+ x86_64-linux
+
+DEPENDENCIES
+ bundler (~> 2.2)
+ ipynbdiff!
+ pry (~> 0.14)
+ rake (~> 13.0)
+ rspec (~> 3.10)
+ rspec-parameterized (~> 0.5.1)
+
+BUNDLED WITH
+ 2.3.16
diff --git a/vendor/gems/ipynbdiff/LICENSE b/vendor/gems/ipynbdiff/LICENSE
new file mode 100644
index 00000000000..e6de2f90864
--- /dev/null
+++ b/vendor/gems/ipynbdiff/LICENSE
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2016-2021 GitLab B.V.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
diff --git a/vendor/gems/ipynbdiff/README.md b/vendor/gems/ipynbdiff/README.md
new file mode 100644
index 00000000000..f046f678a4d
--- /dev/null
+++ b/vendor/gems/ipynbdiff/README.md
@@ -0,0 +1,56 @@
+# IpynbDiff: Better diff for Jupyter Notebooks
+
+This is a simple diff tool that cleans up Jupyter notebooks, transforming each [notebook](example/1/from.ipynb)
+into a [readable markdown file](example/1/from_html.md), keeping the output of cells, and running the
+diff after. Markdowns are generated using an opinionated Jupyter to Markdown conversion. This means
+that the entire file is readable on the diff.
+
+The result are diffs that are much easier to read:
+
+| Diff | IpynbDiff |
+| ----------------------------------- | ----------------------------------------------------- |
+| [Diff text](example/diff.txt) | [IpynbDiff text](example/ipynbdiff_percent.txt) |
+| ![Diff image](example/img/diff.png) | ![IpynbDiff image](example/img/ipynbdiff_percent.png) |
+
+This started as a port of [ipynbdiff](https://gitlab.com/gitlab-org/incubation-engineering/mlops/poc/ipynbdiff),
+but now has extended functionality although not working as git driver.
+
+## Usage
+
+### Generating diffs
+
+```ruby
+IpynbDiff.diff(from_path, to_path, options)
+```
+
+Options:
+
+```ruby
+@default_transform_options = {
+ preprocess_input: true, # Whether the input should be transformed
+ write_output_to: nil, # Pass a path to save the output to a file
+ format: :text, # These are the formats Diffy accepts https://github.com/samg/diffy
+ sources_are_files: false, # Weather to use the from/to as string or path to a file
+ raise_if_invalid_notebook: false, # Raises an error if the notebooks are invalid, otherwise returns nil
+ transform_options: @default_transform_options, # See below for transform options
+ diff_opts: {
+ include_diff_info: false # These are passed to Diffy https://github.com/samg/diffy
+ }
+}
+```
+
+### Transforming the notebooks
+
+It might be necessary to have the transformed files in addition to the diff.
+
+```ruby
+IpynbDiff.transform(notebook, options)
+```
+
+Options:
+
+```ruby
+@default_transform_options = {
+ include_frontmatter: false, # Whether to include or not the notebook metadata (kernel, language, etc)
+}
+```
diff --git a/vendor/gems/ipynbdiff/ipynbdiff.gemspec b/vendor/gems/ipynbdiff/ipynbdiff.gemspec
new file mode 100644
index 00000000000..9ace051b496
--- /dev/null
+++ b/vendor/gems/ipynbdiff/ipynbdiff.gemspec
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+lib = File.expand_path('lib/..', __dir__)
+$LOAD_PATH.unshift lib unless $LOAD_PATH.include?(lib)
+
+require 'lib/version'
+
+Gem::Specification.new do |s|
+ s.name = 'ipynbdiff'
+ s.version = IpynbDiff::VERSION
+ s.summary = 'Human Readable diffs for Jupyter Notebooks'
+ s.description = 'Better diff for Jupyter Notebooks by first preprocessing them and removing clutter'
+ s.authors = ['Eduardo Bonet']
+ s.email = 'ebonet@gitlab.com'
+ # Specify which files should be added to the gem when it is released.
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
+ s.files = Dir.chdir(File.expand_path(__dir__)) do
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(spec|example)/}) }
+ end
+ s.homepage =
+ 'https://gitlab.com/gitlab-org/incubation-engineering/mlops/rb-ipynbdiff'
+ s.license = 'MIT'
+
+ s.require_paths = ['lib']
+
+ s.add_runtime_dependency 'diffy', '~> 3.3'
+ s.add_runtime_dependency 'json', '~> 2.5', '>= 2.5.1'
+
+ s.add_development_dependency 'bundler', '~> 2.2'
+ s.add_development_dependency 'pry', '~> 0.14'
+ s.add_development_dependency 'rake', '~> 13.0'
+ s.add_development_dependency 'rspec', '~> 3.10'
+ s.add_development_dependency 'rspec-parameterized', '~> 0.5.1'
+end
diff --git a/vendor/gems/ipynbdiff/lib/diff.rb b/vendor/gems/ipynbdiff/lib/diff.rb
new file mode 100644
index 00000000000..3554ac55d99
--- /dev/null
+++ b/vendor/gems/ipynbdiff/lib/diff.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+# Custom differ for Jupyter Notebooks
+module IpynbDiff
+ require 'delegate'
+
+ # The result of a diff object
+ class Diff < SimpleDelegator
+ require 'diffy'
+
+ attr_reader :from, :to
+
+ def initialize(from, to, diffy_opts)
+ super(Diffy::Diff.new(from.as_text, to.as_text, **diffy_opts))
+
+ @from = from
+ @to = to
+ end
+ end
+end
diff --git a/vendor/gems/ipynbdiff/lib/ipynb_symbol_map.rb b/vendor/gems/ipynbdiff/lib/ipynb_symbol_map.rb
new file mode 100644
index 00000000000..33e06aa8d18
--- /dev/null
+++ b/vendor/gems/ipynbdiff/lib/ipynb_symbol_map.rb
@@ -0,0 +1,218 @@
+# frozen_string_literal: true
+
+module IpynbDiff
+ class InvalidTokenError < StandardError
+ end
+
+ # Creates a symbol map for a ipynb file (JSON format)
+ class IpynbSymbolMap
+ class << self
+ def parse(notebook, objects_to_ignore = [])
+ IpynbSymbolMap.new(notebook, objects_to_ignore).parse('')
+ end
+ end
+
+ attr_reader :current_line, :char_idx, :results
+
+ WHITESPACE_CHARS = ["\t", "\r", ' ', "\n"].freeze
+
+ VALUE_STOPPERS = [',', '[', ']', '{', '}', *WHITESPACE_CHARS].freeze
+
+ def initialize(notebook, objects_to_ignore = [])
+ @chars = notebook.chars
+ @current_line = 0
+ @char_idx = 0
+ @results = {}
+ @objects_to_ignore = objects_to_ignore
+ end
+
+ def parse(prefix = '.')
+ raise_if_file_ended
+
+ skip_whitespaces
+
+ if (c = current_char) == '"'
+ parse_string
+ elsif c == '['
+ parse_array(prefix)
+ elsif c == '{'
+ parse_object(prefix)
+ else
+ parse_value
+ end
+
+ results
+ end
+
+ def parse_array(prefix)
+ # [1, 2, {"some": "object"}, [1]]
+
+ i = 0
+
+ current_should_be '['
+
+ loop do
+ raise_if_file_ended
+
+ break if skip_beginning(']')
+
+ new_prefix = "#{prefix}.#{i}"
+
+ add_result(new_prefix, current_line)
+
+ parse(new_prefix)
+
+ i += 1
+ end
+ end
+
+ def parse_object(prefix)
+ # {"name":"value", "another_name": [1, 2, 3]}
+
+ current_should_be '{'
+
+ loop do
+ raise_if_file_ended
+
+ break if skip_beginning('}')
+
+ prop_name = parse_string(return_value: true)
+
+ next_and_skip_whitespaces
+
+ current_should_be ':'
+
+ next_and_skip_whitespaces
+
+ if @objects_to_ignore.include? prop_name
+ skip
+ else
+ new_prefix = "#{prefix}.#{prop_name}"
+
+ add_result(new_prefix, current_line)
+
+ parse(new_prefix)
+ end
+ end
+ end
+
+ def parse_string(return_value: false)
+ current_should_be '"'
+ init_idx = @char_idx
+
+ loop do
+ increment_char_index
+
+ raise_if_file_ended
+
+ if current_char == '"' && !prev_backslash?
+ init_idx += 1
+ break
+ end
+ end
+
+ @chars[init_idx...@char_idx].join if return_value
+ end
+
+ def add_result(key, line_number)
+ @results[key] = line_number
+ end
+
+ def parse_value
+ increment_char_index until raise_if_file_ended || VALUE_STOPPERS.include?(current_char)
+ end
+
+ def skip_whitespaces
+ while WHITESPACE_CHARS.include?(current_char)
+ raise_if_file_ended
+ check_for_new_line
+ increment_char_index
+ end
+ end
+
+ def increment_char_index
+ @char_idx += 1
+ end
+
+ def next_and_skip_whitespaces
+ increment_char_index
+ skip_whitespaces
+ end
+
+ def current_char
+ raise_if_file_ended
+
+ @chars[@char_idx]
+ end
+
+ def prev_backslash?
+ @chars[@char_idx - 1] == '\\' && @chars[@char_idx - 2] != '\\'
+ end
+
+ def current_should_be(another_char)
+ raise InvalidTokenError unless current_char == another_char
+ end
+
+ def check_for_new_line
+ @current_line += 1 if current_char == "\n"
+ end
+
+ def raise_if_file_ended
+ @char_idx >= @chars.size && raise(InvalidTokenError)
+ end
+
+ def skip
+ raise_if_file_ended
+
+ skip_whitespaces
+
+ if (c = current_char) == '"'
+ parse_string
+ elsif c == '['
+ skip_array
+ elsif c == '{'
+ skip_object
+ else
+ parse_value
+ end
+ end
+
+ def skip_array
+ loop do
+ raise_if_file_ended
+
+ break if skip_beginning(']')
+
+ skip
+ end
+ end
+
+ def skip_object
+ loop do
+ raise_if_file_ended
+
+ break if skip_beginning('}')
+
+ parse_string
+
+ next_and_skip_whitespaces
+
+ current_should_be ':'
+
+ next_and_skip_whitespaces
+
+ skip
+ end
+ end
+
+ def skip_beginning(closing_char)
+ check_for_new_line
+
+ next_and_skip_whitespaces
+
+ return true if current_char == closing_char
+
+ next_and_skip_whitespaces if current_char == ','
+ end
+ end
+end
diff --git a/vendor/gems/ipynbdiff/lib/ipynbdiff.rb b/vendor/gems/ipynbdiff/lib/ipynbdiff.rb
new file mode 100644
index 00000000000..1765e434bf9
--- /dev/null
+++ b/vendor/gems/ipynbdiff/lib/ipynbdiff.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+# Human Readable Jupyter Diffs
+module IpynbDiff
+ require 'transformer'
+ require 'diff'
+
+ def self.diff(from, to, raise_if_invalid_nb: false, include_frontmatter: false, hide_images: false, diffy_opts: {})
+ transformer = Transformer.new(include_frontmatter: include_frontmatter, hide_images: hide_images)
+
+ Diff.new(transformer.transform(from), transformer.transform(to), diffy_opts)
+ rescue InvalidNotebookError
+ raise if raise_if_invalid_nb
+ end
+
+ def self.transform(notebook, raise_errors: false, include_frontmatter: true, hide_images: false)
+ return unless notebook
+
+ Transformer.new(include_frontmatter: include_frontmatter, hide_images: hide_images).transform(notebook).as_text
+ rescue InvalidNotebookError
+ raise if raise_errors
+ end
+end
diff --git a/vendor/gems/ipynbdiff/lib/output_transformer.rb b/vendor/gems/ipynbdiff/lib/output_transformer.rb
new file mode 100644
index 00000000000..88728df2f17
--- /dev/null
+++ b/vendor/gems/ipynbdiff/lib/output_transformer.rb
@@ -0,0 +1,83 @@
+# frozen_string_literal: true
+
+module IpynbDiff
+ # Transforms Jupyter output data into markdown
+ class OutputTransformer
+ require 'symbolized_markdown_helper'
+ include SymbolizedMarkdownHelper
+
+ HIDDEN_IMAGE_OUTPUT = ' [Hidden Image Output]'
+
+ ORDERED_KEYS = {
+ 'execute_result' => %w[image/png image/svg+xml image/jpeg text/markdown text/latex text/plain],
+ 'display_data' => %w[image/png image/svg+xml image/jpeg text/markdown text/latex],
+ 'stream' => %w[text]
+ }.freeze
+
+ def initialize(hide_images: false)
+ @hide_images = hide_images
+ end
+
+ def transform(output, symbol)
+ transformed = case (output_type = output['output_type'])
+ when 'error'
+ transform_error(output['traceback'], symbol / 'traceback')
+ when 'execute_result', 'display_data'
+ transform_non_error(ORDERED_KEYS[output_type], output['data'], symbol / 'data')
+ when 'stream'
+ transform_element('text', output['text'], symbol)
+ end
+
+ transformed ? decorate_output(transformed, output, symbol) : []
+ end
+
+ def decorate_output(output_rows, output, symbol)
+ [
+ _,
+ _(symbol, %(%%%% Output: #{output['output_type']})),
+ _,
+ *output_rows
+ ]
+ end
+
+ def transform_error(traceback, symbol)
+ traceback.map.with_index do |t, idx|
+ t.split("\n").map do |l|
+ _(symbol / idx, l.gsub(/\[[0-9][0-9;]*m/, '').sub("\u001B", ' ').gsub(/\u001B/, '').rstrip)
+ end
+ end
+ end
+
+ def transform_non_error(accepted_keys, elements, symbol)
+ accepted_keys.filter { |key| elements.key?(key) }.map do |key|
+ transform_element(key, elements[key], symbol)
+ end
+ end
+
+ def transform_element(output_type, output_element, symbol_prefix)
+ new_symbol = symbol_prefix / output_type
+ case output_type
+ when 'image/png', 'image/jpeg'
+ transform_image(output_type + ';base64', output_element, new_symbol)
+ when 'image/svg+xml'
+ transform_image(output_type + ';utf8', output_element, new_symbol)
+ when 'text/markdown', 'text/latex', 'text/plain', 'text'
+ transform_text(output_element, new_symbol)
+ end
+ end
+
+ def transform_image(image_type, image_content, symbol)
+ return _(nil, HIDDEN_IMAGE_OUTPUT) if @hide_images
+
+ lines = image_content.is_a?(Array) ? image_content : [image_content]
+
+ single_line = lines.map(&:strip).join.gsub(/\s+/, ' ')
+
+ _(symbol, " ![](data:#{image_type},#{single_line})")
+ end
+
+ def transform_text(text_content, symbol)
+ symbolize_array(symbol, text_content) { |l| " #{l.rstrip}" }
+ end
+ end
+end
diff --git a/vendor/gems/ipynbdiff/lib/symbolized_markdown_helper.rb b/vendor/gems/ipynbdiff/lib/symbolized_markdown_helper.rb
new file mode 100644
index 00000000000..918666ed899
--- /dev/null
+++ b/vendor/gems/ipynbdiff/lib/symbolized_markdown_helper.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+module IpynbDiff
+ # Helper functions
+ module SymbolizedMarkdownHelper
+
+ def _(symbol = nil, content = '')
+ { symbol: symbol, content: content }
+ end
+
+ def symbolize_array(symbol, content, &block)
+ if content.is_a?(Array)
+ content.map.with_index { |l, idx| _(symbol / idx, block.call(l)) }
+ else
+ _(symbol, content)
+ end
+ end
+ end
+
+ # Simple wrapper for a string
+ class JsonSymbol < String
+ def /(other)
+ JsonSymbol.new((other.is_a?(Array) ? [self, *other] : [self, other]).join('.'))
+ end
+ end
+end
diff --git a/vendor/gems/ipynbdiff/lib/transformed_notebook.rb b/vendor/gems/ipynbdiff/lib/transformed_notebook.rb
new file mode 100644
index 00000000000..7a8edf7c22f
--- /dev/null
+++ b/vendor/gems/ipynbdiff/lib/transformed_notebook.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+module IpynbDiff
+ # Notebook that was transformed into md, including location of source cells
+ class TransformedNotebook
+ attr_reader :blocks
+
+ def as_text
+ @blocks.map { |b| b[:content] }.join("\n")
+ end
+
+ private
+
+ def initialize(lines = [], symbol_map = {})
+ @blocks = lines.map do |line|
+ { content: line[:content], source_symbol: (symbol = line[:symbol]), source_line: symbol && symbol_map[symbol] }
+ end
+ end
+ end
+end
diff --git a/vendor/gems/ipynbdiff/lib/transformer.rb b/vendor/gems/ipynbdiff/lib/transformer.rb
new file mode 100644
index 00000000000..153d821db27
--- /dev/null
+++ b/vendor/gems/ipynbdiff/lib/transformer.rb
@@ -0,0 +1,101 @@
+# frozen_string_literal: true
+
+module IpynbDiff
+ class InvalidNotebookError < StandardError
+ end
+
+ # Returns a markdown version of the Jupyter Notebook
+ class Transformer
+ require 'json'
+ require 'yaml'
+ require 'output_transformer'
+ require 'symbolized_markdown_helper'
+ require 'ipynb_symbol_map'
+ require 'transformed_notebook'
+ include SymbolizedMarkdownHelper
+
+ @include_frontmatter = true
+ @objects_to_ignore = ['application/javascript', 'application/vnd.holoviews_load.v0+json']
+
+ def initialize(include_frontmatter: true, hide_images: false)
+ @include_frontmatter = include_frontmatter
+ @hide_images = hide_images
+ @output_transformer = OutputTransformer.new(hide_images: hide_images)
+ end
+
+ def validate_notebook(notebook)
+ notebook_json = JSON.parse(notebook)
+
+ return notebook_json if notebook_json.key?('cells')
+
+ raise InvalidNotebookError
+ rescue JSON::ParserError
+ raise InvalidNotebookError
+ end
+
+ def transform(notebook)
+ return TransformedNotebook.new unless notebook
+
+ notebook_json = validate_notebook(notebook)
+ transformed = transform_document(notebook_json)
+ symbol_map = IpynbSymbolMap.parse(notebook)
+
+ TransformedNotebook.new(transformed, symbol_map)
+ end
+
+ def transform_document(notebook)
+ symbol = JsonSymbol.new('.cells')
+
+ transformed_blocks = notebook['cells'].map.with_index do |cell, idx|
+ decorate_cell(transform_cell(cell, notebook, symbol / idx), cell, symbol / idx)
+ end
+
+ transformed_blocks.prepend(transform_metadata(notebook)) if @include_frontmatter
+ transformed_blocks.flatten
+ end
+
+ def decorate_cell(rows, cell, symbol)
+ tags = cell['metadata']&.fetch('tags', [])
+ type = cell['cell_type'] || 'raw'
+
+ [
+ _(symbol, %(%% Cell type:#{type} id:#{cell['id']} tags:#{tags&.join(',')})),
+ _,
+ rows,
+ _
+ ]
+ end
+
+ def transform_cell(cell, notebook, symbol)
+ cell['cell_type'] == 'code' ? transform_code_cell(cell, notebook, symbol) : transform_text_cell(cell, symbol)
+ end
+
+ def transform_code_cell(cell, notebook, symbol)
+ [
+ _(symbol / 'source', %(``` #{notebook.dig('metadata', 'kernelspec', 'language') || ''})),
+ symbolize_array(symbol / 'source', cell['source'], &:rstrip),
+ _(nil, '```'),
+ cell['outputs'].map.with_index do |output, idx|
+ @output_transformer.transform(output, symbol / ['outputs', idx])
+ end
+ ]
+ end
+
+ def transform_text_cell(cell, symbol)
+ symbolize_array(symbol / 'source', cell['source'], &:rstrip)
+ end
+
+ def transform_metadata(notebook_json)
+ as_yaml = {
+ 'jupyter' => {
+ 'kernelspec' => notebook_json['metadata']['kernelspec'],
+ 'language_info' => notebook_json['metadata']['language_info'],
+ 'nbformat' => notebook_json['nbformat'],
+ 'nbformat_minor' => notebook_json['nbformat_minor']
+ }
+ }.to_yaml
+
+ as_yaml.split("\n").map { |l| _(nil, l) }.append(_(nil, '---'), _)
+ end
+ end
+end
diff --git a/vendor/gems/ipynbdiff/lib/version.rb b/vendor/gems/ipynbdiff/lib/version.rb
new file mode 100644
index 00000000000..1451bb4ef32
--- /dev/null
+++ b/vendor/gems/ipynbdiff/lib/version.rb
@@ -0,0 +1,5 @@
+# frozen_string_literal: true
+
+module IpynbDiff
+ VERSION = '0.4.7'
+end
diff --git a/vendor/gems/ipynbdiff/spec/ipynb_symbol_map_spec.rb b/vendor/gems/ipynbdiff/spec/ipynb_symbol_map_spec.rb
new file mode 100644
index 00000000000..a002fc370f5
--- /dev/null
+++ b/vendor/gems/ipynbdiff/spec/ipynb_symbol_map_spec.rb
@@ -0,0 +1,165 @@
+# frozen_string_literal: true
+
+require 'rspec'
+require 'json'
+require 'rspec-parameterized'
+require 'ipynb_symbol_map'
+
+describe IpynbDiff::IpynbSymbolMap do
+ def res(*cases)
+ cases&.to_h || []
+ end
+
+ describe '#parse_string' do
+ using RSpec::Parameterized::TableSyntax
+
+ let(:mapper) { IpynbDiff::IpynbSymbolMap.new(input) }
+
+ where(:input, :result) do
+ # Empty string
+ '""' | ''
+ # Some string with quotes
+ '"he\nll\"o"' | 'he\nll\"o'
+ end
+
+ with_them do
+ it { expect(mapper.parse_string(return_value: true)).to eq(result) }
+ it { expect(mapper.parse_string).to be_nil }
+ it { expect(mapper.results).to be_empty }
+ end
+
+ it 'raises if invalid string' do
+ mapper = IpynbDiff::IpynbSymbolMap.new('"')
+
+ expect { mapper.parse_string }.to raise_error(IpynbDiff::InvalidTokenError)
+ end
+
+ end
+
+ describe '#parse_object' do
+ using RSpec::Parameterized::TableSyntax
+
+ let(:mapper) { IpynbDiff::IpynbSymbolMap.new(notebook, objects_to_ignore) }
+
+ before do
+ mapper.parse_object('')
+ end
+
+ where(:notebook, :objects_to_ignore, :result) do
+ # Empty object
+ '{ }' | [] | res
+ # Object with string
+ '{ "hello" : "world" }' | [] | res(['.hello', 0])
+ # Object with boolean
+ '{ "hello" : true }' | [] | res(['.hello', 0])
+ # Object with integer
+ '{ "hello" : 1 }' | [] | res(['.hello', 0])
+ # Object with 2 properties in the same line
+ '{ "hello" : "world" , "my" : "bad" }' | [] | res(['.hello', 0], ['.my', 0])
+ # Object with 2 properties in the different lines line
+ "{ \"hello\" : \"world\" , \n \n \"my\" : \"bad\" }" | [] | res(['.hello', 0], ['.my', 2])
+ # Object with 2 properties, but one is ignored
+ "{ \"hello\" : \"world\" , \n \n \"my\" : \"bad\" }" | ['hello'] | res(['.my', 2])
+ end
+
+ with_them do
+ it { expect(mapper.results).to include(result) }
+ end
+ end
+
+ describe '#parse_array' do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:notebook, :result) do
+ # Empty Array
+ '[]' | res
+ # Array with string value
+ '["a"]' | res(['.0', 0])
+ # Array with boolean
+ '[ true ]' | res(['.0', 0])
+ # Array with integer
+ '[ 1 ]' | res(['.0', 0])
+ # Two values on the same line
+ '["a", "b"]' | res(['.0', 0], ['.1', 0])
+ # With line breaks'
+ "[\n \"a\" \n , \n \"b\" ]" | res(['.0', 1], ['.1', 3])
+ end
+
+ let(:mapper) { IpynbDiff::IpynbSymbolMap.new(notebook) }
+
+ before do
+ mapper.parse_array('')
+ end
+
+ with_them do
+ it { expect(mapper.results).to match_array(result) }
+ end
+ end
+
+ describe '#skip_object' do
+ subject { IpynbDiff::IpynbSymbolMap.parse(JSON.pretty_generate(source)) }
+ end
+
+ describe '#parse' do
+
+ let(:objects_to_ignore) { [] }
+
+ subject { IpynbDiff::IpynbSymbolMap.parse(JSON.pretty_generate(source), objects_to_ignore) }
+
+ context 'Empty object' do
+ let(:source) { {} }
+
+ it { is_expected.to be_empty }
+ end
+
+ context 'Object with inner object and number' do
+ let(:source) { { obj1: { obj2: 1 } } }
+
+ it { is_expected.to match_array(res(['.obj1', 1], ['.obj1.obj2', 2])) }
+ end
+
+ context 'Object with inner object and number, string and array with object' do
+ let(:source) { { obj1: { obj2: [123, 2, true], obj3: "hel\nlo", obj4: true, obj5: 123, obj6: 'a' } } }
+
+ it do
+ is_expected.to match_array(
+ res(['.obj1', 1],
+ ['.obj1.obj2', 2],
+ ['.obj1.obj2.0', 3],
+ ['.obj1.obj2.1', 4],
+ ['.obj1.obj2.2', 5],
+ ['.obj1.obj3', 7],
+ ['.obj1.obj4', 8],
+ ['.obj1.obj5', 9],
+ ['.obj1.obj6', 10])
+ )
+ end
+ end
+
+ context 'When index is exceeded because of failure' do
+ it 'raises an exception' do
+ source = '{"\\a": "a\""}'
+
+ mapper = IpynbDiff::IpynbSymbolMap.new(source)
+
+ expect(mapper).to receive(:prev_backslash?).at_least(1).time.and_return(false)
+
+ expect { mapper.parse('') }.to raise_error(IpynbDiff::InvalidTokenError)
+ end
+ end
+
+ context 'Object with inner object and number, string and array with object' do
+ let(:source) { { obj1: { obj2: [123, 2, true], obj3: "hel\nlo", obj4: true, obj5: 123, obj6: { obj7: 'a' } } } }
+ let(:objects_to_ignore) { %w(obj2 obj6) }
+ it do
+ is_expected.to match_array(
+ res(['.obj1', 1],
+ ['.obj1.obj3', 7],
+ ['.obj1.obj4', 8],
+ ['.obj1.obj5', 9],
+ )
+ )
+ end
+ end
+ end
+end
diff --git a/vendor/gems/ipynbdiff/spec/ipynbdiff_spec.rb b/vendor/gems/ipynbdiff/spec/ipynbdiff_spec.rb
new file mode 100644
index 00000000000..1c2a2188edf
--- /dev/null
+++ b/vendor/gems/ipynbdiff/spec/ipynbdiff_spec.rb
@@ -0,0 +1,126 @@
+# frozen_string_literal: true
+
+require 'ipynbdiff'
+require 'rspec'
+require 'rspec-parameterized'
+
+BASE_PATH = File.join(File.expand_path(File.dirname(__FILE__)), 'testdata')
+
+describe IpynbDiff do
+ def diff_signs(diff)
+ diff.to_s(:text).scan(/.*\n/).map { |l| l[0] }.join('')
+ end
+
+ describe 'diff' do
+ let(:from_path) { File.join(BASE_PATH, 'from.ipynb') }
+ let(:to_path) { File.join(BASE_PATH,'to.ipynb') }
+ let(:from) { File.read(from_path) }
+ let(:to) { File.read(to_path) }
+ let(:include_frontmatter) { false }
+ let(:hide_images) { false }
+
+ subject { IpynbDiff.diff(from, to, include_frontmatter: include_frontmatter, hide_images: hide_images) }
+
+ context 'if preprocessing is active' do
+ it 'html tables are stripped' do
+ is_expected.to_not include('<td>')
+ end
+ end
+
+ context 'when to is nil' do
+ let(:to) { nil }
+ let(:from_path) { File.join(BASE_PATH, 'only_md', 'input.ipynb') }
+
+ it 'all lines are removals' do
+ expect(diff_signs(subject)).to eq('-----')
+ end
+ end
+
+ context 'when to is nil' do
+ let(:from) { nil }
+ let(:to_path) { File.join(BASE_PATH, 'only_md', 'input.ipynb') }
+
+ it 'all lines are additions' do
+ expect(diff_signs(subject)).to eq('+++++')
+ end
+ end
+
+ context 'When include_frontmatter is true' do
+ let(:include_frontmatter) { true }
+
+ it 'should show changes metadata in the metadata' do
+ expect(subject.to_s(:text)).to include('+ display_name: New Python 3 (ipykernel)')
+ end
+ end
+
+ context 'When hide_images is true' do
+ let(:hide_images) { true }
+
+ it 'hides images' do
+ expect(subject.to_s(:text)).to include(' [Hidden Image Output]')
+ end
+ end
+
+ context 'When include_frontmatter is false' do
+ it 'should drop metadata from the diff' do
+ expect(subject.to_s(:text)).to_not include('+ display_name: New Python 3 (ipykernel)')
+ end
+ end
+
+ context 'when either notebook can not be processed' do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:ctx, :from, :to) do
+ 'because from is invalid' | 'a' | nil
+ 'because from does not have the cell tag' | '{"metadata":[]}' | nil
+ 'because to is invalid' | nil | 'a'
+ 'because to does not have the cell tag' | nil | '{"metadata":[]}'
+ end
+
+ with_them do
+ it { is_expected.to be_nil }
+ end
+ end
+ end
+
+ describe 'transform' do
+ [nil, 'a', '{"metadata":[]}'].each do |invalid_nb|
+ context "when json is invalid (#{invalid_nb || 'nil'})" do
+ it 'is nil' do
+ expect(IpynbDiff.transform(invalid_nb)).to be_nil
+ end
+ end
+ end
+
+ context 'options' do
+ let(:include_frontmatter) { false }
+ let(:hide_images) { false }
+
+ subject do
+ IpynbDiff.transform(File.read(File.join(BASE_PATH, 'from.ipynb')),
+ include_frontmatter: include_frontmatter,
+ hide_images: hide_images)
+ end
+
+ context 'include_frontmatter is false' do
+ it { is_expected.to_not include('display_name: Python 3 (ipykernel)') }
+ end
+
+ context 'include_frontmatter is true' do
+ let(:include_frontmatter) { true }
+
+ it { is_expected.to include('display_name: Python 3 (ipykernel)') }
+ end
+
+ context 'hide_images is false' do
+ it { is_expected.not_to include('[Hidden Image Output]') }
+ end
+
+ context 'hide_images is true' do
+ let(:hide_images) { true }
+
+ it { is_expected.to include(' [Hidden Image Output]') }
+ end
+ end
+ end
+end
diff --git a/vendor/gems/ipynbdiff/spec/testdata/backslash_as_last_char/expected.md b/vendor/gems/ipynbdiff/spec/testdata/backslash_as_last_char/expected.md
new file mode 100644
index 00000000000..299e286c679
--- /dev/null
+++ b/vendor/gems/ipynbdiff/spec/testdata/backslash_as_last_char/expected.md
@@ -0,0 +1,7 @@
+%% Cell type:markdown id: tags:
+
+\
+
+%% Cell type:markdown id: tags:
+
+a
diff --git a/vendor/gems/ipynbdiff/spec/testdata/backslash_as_last_char/expected_symbols.txt b/vendor/gems/ipynbdiff/spec/testdata/backslash_as_last_char/expected_symbols.txt
new file mode 100644
index 00000000000..6fa29ae28de
--- /dev/null
+++ b/vendor/gems/ipynbdiff/spec/testdata/backslash_as_last_char/expected_symbols.txt
@@ -0,0 +1,7 @@
+.cells.0
+
+.cells.0.source.0
+
+.cells.1
+
+.cells.1.source.0
diff --git a/vendor/gems/ipynbdiff/spec/testdata/backslash_as_last_char/input.ipynb b/vendor/gems/ipynbdiff/spec/testdata/backslash_as_last_char/input.ipynb
new file mode 100644
index 00000000000..0714044e3ae
--- /dev/null
+++ b/vendor/gems/ipynbdiff/spec/testdata/backslash_as_last_char/input.ipynb
@@ -0,0 +1,16 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "source": [
+ "\\"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "source": [
+ "a"
+ ]
+ }
+ ]
+}
diff --git a/vendor/gems/ipynbdiff/spec/testdata/error_output/expected.md b/vendor/gems/ipynbdiff/spec/testdata/error_output/expected.md
new file mode 100644
index 00000000000..5be645de9c9
--- /dev/null
+++ b/vendor/gems/ipynbdiff/spec/testdata/error_output/expected.md
@@ -0,0 +1,16 @@
+%% Cell type:code id:5 tags:
+
+``` python
+# A cell that has an error
+y = sin(x)
+```
+
+%%%% Output: error
+
+ ---------------------------------------------------------------------------
+ NameError Traceback (most recent call last)
+ /var/folders/cq/l637k4x13gx6y9p_gfs4c_gc0000gn/T/ipykernel_72857/3962062127.py in <module>
+ 1 # A cell that has an error
+ ----> 2 y = sin(x)
+
+ NameError: name 'sin' is not defined
diff --git a/vendor/gems/ipynbdiff/spec/testdata/error_output/expected_symbols.txt b/vendor/gems/ipynbdiff/spec/testdata/error_output/expected_symbols.txt
new file mode 100644
index 00000000000..75e35d123d0
--- /dev/null
+++ b/vendor/gems/ipynbdiff/spec/testdata/error_output/expected_symbols.txt
@@ -0,0 +1,16 @@
+.cells.0
+
+.cells.0.source
+.cells.0.source.0
+.cells.0.source.1
+
+
+.cells.0.outputs.0
+
+.cells.0.outputs.0.traceback.0
+.cells.0.outputs.0.traceback.1
+.cells.0.outputs.0.traceback.2
+.cells.0.outputs.0.traceback.2
+.cells.0.outputs.0.traceback.2
+.cells.0.outputs.0.traceback.2
+.cells.0.outputs.0.traceback.3
diff --git a/vendor/gems/ipynbdiff/spec/testdata/error_output/input.ipynb b/vendor/gems/ipynbdiff/spec/testdata/error_output/input.ipynb
new file mode 100644
index 00000000000..45ee81a0e2d
--- /dev/null
+++ b/vendor/gems/ipynbdiff/spec/testdata/error_output/input.ipynb
@@ -0,0 +1,32 @@
+{
+ "cells": [
+ {
+ "cell_type": "code",
+ "execution_count": 7,
+ "id": "5",
+ "metadata": {},
+ "outputs": [
+ {
+ "ename": "NameError",
+ "evalue": "name 'sin' is not defined",
+ "output_type": "error",
+ "traceback": [
+ "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m",
+ "\u001b[0;31mNameError\u001b[0m Traceback (most recent call last)",
+ "\u001b[0;32m/var/folders/cq/l637k4x13gx6y9p_gfs4c_gc0000gn/T/ipykernel_72857/3962062127.py\u001b[0m in \u001b[0;36m<module>\u001b[0;34m\u001b[0m\n\u001b[1;32m 1\u001b[0m \u001b[0;31m# A cell that has an error\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m----> 2\u001b[0;31m \u001b[0my\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0msin\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mx\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m",
+ "\u001b[0;31mNameError\u001b[0m: name 'sin' is not defined"
+ ]
+ }
+ ],
+ "source": [
+ "# A cell that has an error\n",
+ "y = sin(x)"
+ ]
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "language": "python"
+ }
+ }
+}
diff --git a/vendor/gems/ipynbdiff/spec/testdata/from.ipynb b/vendor/gems/ipynbdiff/spec/testdata/from.ipynb
new file mode 100644
index 00000000000..a731c9bfffd
--- /dev/null
+++ b/vendor/gems/ipynbdiff/spec/testdata/from.ipynb
@@ -0,0 +1,198 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "id": "0aac5da7-745c-4eda-847a-3d0d07a1bb9b",
+ "metadata": {
+ "tags": []
+ },
+ "source": [
+ "# This is a markdown cell\n",
+ "\n",
+ "This paragraph has\n",
+ "With\n",
+ "Many\n",
+ "Lines. How we will he handle MR notes?\n",
+ "\n",
+ "But I can add another paragraph"
+ ]
+ },
+ {
+ "cell_type": "raw",
+ "id": "faecea5b-de0a-49fa-9a3a-61c2add652da",
+ "metadata": {},
+ "source": [
+ "This is a raw cell\n",
+ "With\n",
+ "Multiple lines"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 1,
+ "id": "893ca2c0-ab75-4276-9dad-be1c40e16e8a",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "import pandas as pd\n",
+ "import numpy as np\n",
+ "import matplotlib.pyplot as plt"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 2,
+ "id": "0d707fb5-226f-46d6-80bd-489ebfb8905c",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "np.random.seed(42)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 3,
+ "id": "35467fcf-28b1-4c7b-bb09-4cb192c35293",
+ "metadata": {
+ "tags": [
+ "senoid"
+ ]
+ },
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "[<matplotlib.lines.Line2D at 0x123e39370>]"
+ ]
+ },
+ "execution_count": 3,
+ "metadata": {},
+ "output_type": "execute_result"
+ },
+ {
+ "data": {
+ "image/png": "some_invalid_base64_image_here\n",
+ "text/plain": [
+ "<Figure size 432x288 with 1 Axes>"
+ ]
+ },
+ "metadata": {
+ "needs_background": "light"
+ },
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "x = np.linspace(0, 4*np.pi,50)\n",
+ "y = np.sin(x)\n",
+ "\n",
+ "plt.plot(x, y)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 4,
+ "id": "dc1178cd-c46d-4da3-9ab5-08f000699884",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "df = pd.DataFrame({\"x\": x, \"y\": y})"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 5,
+ "id": "6e749b4f-b409-4700-870f-f68c39462490",
+ "metadata": {
+ "tags": [
+ "some-table"
+ ]
+ },
+ "outputs": [
+ {
+ "data": {
+ "text/html": [
+ "<div>\n",
+ "<style scoped>\n",
+ " .dataframe tbody tr th:only-of-type {\n",
+ " vertical-align: middle;\n",
+ " }\n",
+ "\n",
+ " .dataframe tbody tr th {\n",
+ " vertical-align: top;\n",
+ " }\n",
+ "\n",
+ " .dataframe thead th {\n",
+ " text-align: right;\n",
+ " }\n",
+ "</style>\n",
+ "<table border=\"1\" class=\"dataframe\">\n",
+ " <thead>\n",
+ " <tr style=\"text-align: right;\">\n",
+ " <th></th>\n",
+ " <th>x</th>\n",
+ " <th>y</th>\n",
+ " </tr>\n",
+ " </thead>\n",
+ " <tbody>\n",
+ " <tr>\n",
+ " <th>0</th>\n",
+ " <td>0.000000</td>\n",
+ " <td>0.000000</td>\n",
+ " </tr>\n",
+ " <tr>\n",
+ " <th>1</th>\n",
+ " <td>0.256457</td>\n",
+ " <td>0.253655</td>\n",
+ " </tr>\n",
+ " </tbody>\n",
+ "</table>\n",
+ "</div>"
+ ],
+ "text/plain": [
+ " x y\n",
+ "0 0.000000 0.000000\n",
+ "1 0.256457 0.253655"
+ ]
+ },
+ "execution_count": 5,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "df[:2]"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "0ddef5ef-94a3-4afd-9c70-ddee9694f512",
+ "metadata": {},
+ "outputs": [],
+ "source": []
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": "Python 3 (ipykernel)",
+ "language": "python",
+ "name": "python3"
+ },
+ "language_info": {
+ "codemirror_mode": {
+ "name": "ipython",
+ "version": 3
+ },
+ "file_extension": ".py",
+ "mimetype": "text/x-python",
+ "name": "python",
+ "nbconvert_exporter": "python",
+ "pygments_lexer": "ipython3",
+ "version": "3.9.7"
+ },
+ "toc-showtags": true
+ },
+ "nbformat": 4,
+ "nbformat_minor": 5
+}
diff --git a/vendor/gems/ipynbdiff/spec/testdata/hide_images/expected.md b/vendor/gems/ipynbdiff/spec/testdata/hide_images/expected.md
new file mode 100644
index 00000000000..89a812740a6
--- /dev/null
+++ b/vendor/gems/ipynbdiff/spec/testdata/hide_images/expected.md
@@ -0,0 +1,12 @@
+%% Cell type:code id:5 tags:senoid
+
+``` python
+```
+
+%%%% Output: display_data
+
+ [Hidden Image Output]
+
+%%%% Output: display_data
+
+ [Hidden Image Output]
diff --git a/vendor/gems/ipynbdiff/spec/testdata/hide_images/expected_symbols.txt b/vendor/gems/ipynbdiff/spec/testdata/hide_images/expected_symbols.txt
new file mode 100644
index 00000000000..b94e9538f58
--- /dev/null
+++ b/vendor/gems/ipynbdiff/spec/testdata/hide_images/expected_symbols.txt
@@ -0,0 +1,12 @@
+.cells.0
+
+.cells.0.source
+
+
+.cells.0.outputs.0
+
+
+
+.cells.0.outputs.1
+
+
diff --git a/vendor/gems/ipynbdiff/spec/testdata/hide_images/input.ipynb b/vendor/gems/ipynbdiff/spec/testdata/hide_images/input.ipynb
new file mode 100644
index 00000000000..dab0e5bb9cf
--- /dev/null
+++ b/vendor/gems/ipynbdiff/spec/testdata/hide_images/input.ipynb
@@ -0,0 +1,45 @@
+{
+ "cells": [
+ {
+ "cell_type": "code",
+ "execution_count": 3,
+ "id": "5",
+ "metadata": {
+ "tags": [
+ "senoid"
+ ]
+ },
+ "outputs": [
+ {
+ "data": {
+ "image/png": "this_is_an_invalid_hash_for_testing_purposes\n",
+ "text/plain": [
+ "<Figure size 432x288 with 1 Axes>"
+ ]
+ },
+ "metadata": {
+ "needs_background": "light"
+ },
+ "output_type": "display_data"
+ },
+ {
+ "data": {
+ "image/svg+xml": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 100 100\"><circle cx=\"50\" cy=\"50\" r=\"50\"/></svg>",
+ "text/plain": [
+ "<IPython.core.display.SVG object>"
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ ]
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "language": "python"
+ }
+ }
+}
diff --git a/vendor/gems/ipynbdiff/spec/testdata/ignore_html_output/expected.md b/vendor/gems/ipynbdiff/spec/testdata/ignore_html_output/expected.md
new file mode 100644
index 00000000000..456224f3aff
--- /dev/null
+++ b/vendor/gems/ipynbdiff/spec/testdata/ignore_html_output/expected.md
@@ -0,0 +1,11 @@
+%% Cell type:code id:5 tags:some-table
+
+``` python
+df[:2]
+```
+
+%%%% Output: execute_result
+
+ x y
+ 0 0.000000 0.000000
+ 1 0.256457 0.507309
diff --git a/vendor/gems/ipynbdiff/spec/testdata/ignore_html_output/expected_symbols.txt b/vendor/gems/ipynbdiff/spec/testdata/ignore_html_output/expected_symbols.txt
new file mode 100644
index 00000000000..fa9d412c6dc
--- /dev/null
+++ b/vendor/gems/ipynbdiff/spec/testdata/ignore_html_output/expected_symbols.txt
@@ -0,0 +1,11 @@
+.cells.0
+
+.cells.0.source
+.cells.0.source.0
+
+
+.cells.0.outputs.0
+
+.cells.0.outputs.0.data.text/plain.0
+.cells.0.outputs.0.data.text/plain.1
+.cells.0.outputs.0.data.text/plain.2
diff --git a/vendor/gems/ipynbdiff/spec/testdata/ignore_html_output/input.ipynb b/vendor/gems/ipynbdiff/spec/testdata/ignore_html_output/input.ipynb
new file mode 100644
index 00000000000..26117a78934
--- /dev/null
+++ b/vendor/gems/ipynbdiff/spec/testdata/ignore_html_output/input.ipynb
@@ -0,0 +1,74 @@
+{
+ "cells": [
+ {
+ "cell_type": "code",
+ "execution_count": 5,
+ "id": "5",
+ "metadata": {
+ "tags": [
+ "some-table"
+ ]
+ },
+ "outputs": [
+ {
+ "data": {
+ "text/html": [
+ "<div>\n",
+ "<style scoped>\n",
+ " .dataframe tbody tr th:only-of-type {\n",
+ " vertical-align: middle;\n",
+ " }\n",
+ "\n",
+ " .dataframe tbody tr th {\n",
+ " vertical-align: top;\n",
+ " }\n",
+ "\n",
+ " .dataframe thead th {\n",
+ " text-align: right;\n",
+ " }\n",
+ "</style>\n",
+ "<table border=\"1\" class=\"dataframe\">\n",
+ " <thead>\n",
+ " <tr style=\"text-align: right;\">\n",
+ " <th></th>\n",
+ " <th>x</th>\n",
+ " <th>y</th>\n",
+ " </tr>\n",
+ " </thead>\n",
+ " <tbody>\n",
+ " <tr>\n",
+ " <th>0</th>\n",
+ " <td>0.000000</td>\n",
+ " <td>0.000000</td>\n",
+ " </tr>\n",
+ " <tr>\n",
+ " <th>1</th>\n",
+ " <td>0.256457</td>\n",
+ " <td>0.507309</td>\n",
+ " </tr>\n",
+ " </tbody>\n",
+ "</table>\n",
+ "</div>"
+ ],
+ "text/plain": [
+ " x y\n",
+ "0 0.000000 0.000000\n",
+ "1 0.256457 0.507309"
+ ]
+ },
+ "execution_count": 5,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "df[:2]"
+ ]
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "language": "python"
+ }
+ }
+}
diff --git a/vendor/gems/ipynbdiff/spec/testdata/latex_output/expected.md b/vendor/gems/ipynbdiff/spec/testdata/latex_output/expected.md
new file mode 100644
index 00000000000..add84ed26a0
--- /dev/null
+++ b/vendor/gems/ipynbdiff/spec/testdata/latex_output/expected.md
@@ -0,0 +1,10 @@
+%% Cell type:code id:5 tags:
+
+``` python
+from IPython.display import display, Math
+display(Math(r'Dims: {}x{}m \\ Area: {}m^2 \\ Volume: {}m^3'.format(1, round(2,2), 3, 4)))
+```
+
+%%%% Output: display_data
+
+ $\displaystyle Dims: 1x2m \\ Area: 3m^2 \\ Volume: 4m^3$
diff --git a/vendor/gems/ipynbdiff/spec/testdata/latex_output/expected_symbols.txt b/vendor/gems/ipynbdiff/spec/testdata/latex_output/expected_symbols.txt
new file mode 100644
index 00000000000..9407e6db702
--- /dev/null
+++ b/vendor/gems/ipynbdiff/spec/testdata/latex_output/expected_symbols.txt
@@ -0,0 +1,10 @@
+.cells.0
+
+.cells.0.source
+.cells.0.source.0
+.cells.0.source.1
+
+
+.cells.0.outputs.0
+
+.cells.0.outputs.0.data.text/latex.0
diff --git a/vendor/gems/ipynbdiff/spec/testdata/latex_output/input.ipynb b/vendor/gems/ipynbdiff/spec/testdata/latex_output/input.ipynb
new file mode 100644
index 00000000000..f8ff3e72beb
--- /dev/null
+++ b/vendor/gems/ipynbdiff/spec/testdata/latex_output/input.ipynb
@@ -0,0 +1,34 @@
+{
+ "cells": [
+ {
+ "cell_type": "code",
+ "execution_count": 4,
+ "id": "5",
+ "outputs": [
+ {
+ "data": {
+ "text/latex": [
+ "$\\displaystyle Dims: 1x2m \\\\ Area: 3m^2 \\\\ Volume: 4m^3$"
+ ],
+ "text/plain": [
+ "<IPython.core.display.Math object>"
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "from IPython.display import display, Math\n",
+ "display(Math(r'Dims: {}x{}m \\\\ Area: {}m^2 \\\\ Volume: {}m^3'.format(1, round(2,2), 3, 4)))"
+ ]
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": "Python 3 (ipykernel)",
+ "language": "python",
+ "name": "python3"
+ }
+ }
+}
diff --git a/vendor/gems/ipynbdiff/spec/testdata/multiline_png_output/expected.md b/vendor/gems/ipynbdiff/spec/testdata/multiline_png_output/expected.md
new file mode 100644
index 00000000000..4a880d8ce18
--- /dev/null
+++ b/vendor/gems/ipynbdiff/spec/testdata/multiline_png_output/expected.md
@@ -0,0 +1,9 @@
+%% Cell type:code id:5 tags:
+
+```
+Some Image
+```
+
+%%%% Output: display_data
+
+ ![](data:image/png;base64,this_is_an_invalid_hash_for_testing_purposes)
diff --git a/vendor/gems/ipynbdiff/spec/testdata/multiline_png_output/expected_symbols.txt b/vendor/gems/ipynbdiff/spec/testdata/multiline_png_output/expected_symbols.txt
new file mode 100644
index 00000000000..26e11781ec1
--- /dev/null
+++ b/vendor/gems/ipynbdiff/spec/testdata/multiline_png_output/expected_symbols.txt
@@ -0,0 +1,9 @@
+.cells.0
+
+.cells.0.source
+.cells.0.source.0
+
+
+.cells.0.outputs.0
+
+.cells.0.outputs.0.data.image/png
diff --git a/vendor/gems/ipynbdiff/spec/testdata/multiline_png_output/input.ipynb b/vendor/gems/ipynbdiff/spec/testdata/multiline_png_output/input.ipynb
new file mode 100644
index 00000000000..4d19a504553
--- /dev/null
+++ b/vendor/gems/ipynbdiff/spec/testdata/multiline_png_output/input.ipynb
@@ -0,0 +1,25 @@
+{
+ "cells": [
+ {
+ "cell_type": "code",
+ "id": "5",
+ "metadata": {
+ },
+ "outputs": [
+ {
+ "data": {
+ "image/png": [
+ "this_is_an_invalid_hash_for_testing_purposes"
+ ]
+ },
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "Some Image"
+ ]
+ }
+ ],
+ "metadata": {
+ }
+}
diff --git a/vendor/gems/ipynbdiff/spec/testdata/no_cells/expected.md b/vendor/gems/ipynbdiff/spec/testdata/no_cells/expected.md
new file mode 100644
index 00000000000..b7c09c51fb8
--- /dev/null
+++ b/vendor/gems/ipynbdiff/spec/testdata/no_cells/expected.md
@@ -0,0 +1,19 @@
+---
+jupyter:
+ kernelspec:
+ display_name: Python 3 (ipykernel)
+ language: python
+ name: python3
+ language_info:
+ codemirror_mode:
+ name: ipython
+ version: 3
+ file_extension: ".py"
+ mimetype: text/x-python
+ name: python
+ nbconvert_exporter: python
+ pygments_lexer: ipython3
+ version: 3.9.7
+ nbformat: 4
+ nbformat_minor: 5
+---
diff --git a/vendor/gems/ipynbdiff/spec/testdata/no_cells/expected_symbols.txt b/vendor/gems/ipynbdiff/spec/testdata/no_cells/expected_symbols.txt
new file mode 100644
index 00000000000..a60f3032882
--- /dev/null
+++ b/vendor/gems/ipynbdiff/spec/testdata/no_cells/expected_symbols.txt
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/vendor/gems/ipynbdiff/spec/testdata/no_cells/input.ipynb b/vendor/gems/ipynbdiff/spec/testdata/no_cells/input.ipynb
new file mode 100644
index 00000000000..c2ba0ebf50a
--- /dev/null
+++ b/vendor/gems/ipynbdiff/spec/testdata/no_cells/input.ipynb
@@ -0,0 +1,25 @@
+{
+ "cells": [],
+ "metadata": {
+ "kernelspec": {
+ "display_name": "Python 3 (ipykernel)",
+ "language": "python",
+ "name": "python3"
+ },
+ "language_info": {
+ "codemirror_mode": {
+ "name": "ipython",
+ "version": 3
+ },
+ "file_extension": ".py",
+ "mimetype": "text/x-python",
+ "name": "python",
+ "nbconvert_exporter": "python",
+ "pygments_lexer": "ipython3",
+ "version": "3.9.7"
+ },
+ "toc-showtags": true
+ },
+ "nbformat": 4,
+ "nbformat_minor": 5
+}
diff --git a/vendor/gems/ipynbdiff/spec/testdata/no_cells_no_metadata/expected.md b/vendor/gems/ipynbdiff/spec/testdata/no_cells_no_metadata/expected.md
new file mode 100644
index 00000000000..e69de29bb2d
--- /dev/null
+++ b/vendor/gems/ipynbdiff/spec/testdata/no_cells_no_metadata/expected.md
diff --git a/vendor/gems/ipynbdiff/spec/testdata/no_cells_no_metadata/expected_symbols.txt b/vendor/gems/ipynbdiff/spec/testdata/no_cells_no_metadata/expected_symbols.txt
new file mode 100644
index 00000000000..e69de29bb2d
--- /dev/null
+++ b/vendor/gems/ipynbdiff/spec/testdata/no_cells_no_metadata/expected_symbols.txt
diff --git a/vendor/gems/ipynbdiff/spec/testdata/no_cells_no_metadata/input.ipynb b/vendor/gems/ipynbdiff/spec/testdata/no_cells_no_metadata/input.ipynb
new file mode 100644
index 00000000000..c2ba0ebf50a
--- /dev/null
+++ b/vendor/gems/ipynbdiff/spec/testdata/no_cells_no_metadata/input.ipynb
@@ -0,0 +1,25 @@
+{
+ "cells": [],
+ "metadata": {
+ "kernelspec": {
+ "display_name": "Python 3 (ipykernel)",
+ "language": "python",
+ "name": "python3"
+ },
+ "language_info": {
+ "codemirror_mode": {
+ "name": "ipython",
+ "version": 3
+ },
+ "file_extension": ".py",
+ "mimetype": "text/x-python",
+ "name": "python",
+ "nbconvert_exporter": "python",
+ "pygments_lexer": "ipython3",
+ "version": "3.9.7"
+ },
+ "toc-showtags": true
+ },
+ "nbformat": 4,
+ "nbformat_minor": 5
+}
diff --git a/vendor/gems/ipynbdiff/spec/testdata/no_metadata_on_cell/expected.md b/vendor/gems/ipynbdiff/spec/testdata/no_metadata_on_cell/expected.md
new file mode 100644
index 00000000000..d9d72bf8f76
--- /dev/null
+++ b/vendor/gems/ipynbdiff/spec/testdata/no_metadata_on_cell/expected.md
@@ -0,0 +1,13 @@
+%% Cell type:markdown id:1 tags:
+
+# A
+
+B
+
+%% Cell type:code id:3 tags:
+
+``` python
+import pandas as pd
+import numpy as np
+import matplotlib.pyplot as plt
+```
diff --git a/vendor/gems/ipynbdiff/spec/testdata/no_metadata_on_cell/expected_symbols.txt b/vendor/gems/ipynbdiff/spec/testdata/no_metadata_on_cell/expected_symbols.txt
new file mode 100644
index 00000000000..a7000494a1b
--- /dev/null
+++ b/vendor/gems/ipynbdiff/spec/testdata/no_metadata_on_cell/expected_symbols.txt
@@ -0,0 +1,13 @@
+.cells.0
+
+.cells.0.source.0
+.cells.0.source.1
+.cells.0.source.2
+
+.cells.1
+
+.cells.1.source
+.cells.1.source.0
+.cells.1.source.1
+.cells.1.source.2
+
diff --git a/vendor/gems/ipynbdiff/spec/testdata/no_metadata_on_cell/input.ipynb b/vendor/gems/ipynbdiff/spec/testdata/no_metadata_on_cell/input.ipynb
new file mode 100644
index 00000000000..62060124a2a
--- /dev/null
+++ b/vendor/gems/ipynbdiff/spec/testdata/no_metadata_on_cell/input.ipynb
@@ -0,0 +1,29 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "id": "1",
+ "source": [
+ "# A\n",
+ "\n",
+ "B"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 1,
+ "id": "3",
+ "outputs": [],
+ "source": [
+ "import pandas as pd\n",
+ "import numpy as np\n",
+ "import matplotlib.pyplot as plt"
+ ]
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "language": "python"
+ }
+ }
+}
diff --git a/vendor/gems/ipynbdiff/spec/testdata/only_code/expected.md b/vendor/gems/ipynbdiff/spec/testdata/only_code/expected.md
new file mode 100644
index 00000000000..124b8217a6a
--- /dev/null
+++ b/vendor/gems/ipynbdiff/spec/testdata/only_code/expected.md
@@ -0,0 +1,7 @@
+%% Cell type:code id:3 tags:
+
+``` python
+import pandas as pd
+import numpy as np
+import matplotlib.pyplot as plt
+```
diff --git a/vendor/gems/ipynbdiff/spec/testdata/only_code/expected_symbols.txt b/vendor/gems/ipynbdiff/spec/testdata/only_code/expected_symbols.txt
new file mode 100644
index 00000000000..59b11103195
--- /dev/null
+++ b/vendor/gems/ipynbdiff/spec/testdata/only_code/expected_symbols.txt
@@ -0,0 +1,7 @@
+.cells.0
+
+.cells.0.source
+.cells.0.source.0
+.cells.0.source.1
+.cells.0.source.2
+
diff --git a/vendor/gems/ipynbdiff/spec/testdata/only_code/input.ipynb b/vendor/gems/ipynbdiff/spec/testdata/only_code/input.ipynb
new file mode 100644
index 00000000000..a93108dccb8
--- /dev/null
+++ b/vendor/gems/ipynbdiff/spec/testdata/only_code/input.ipynb
@@ -0,0 +1,21 @@
+{
+ "cells": [
+ {
+ "cell_type": "code",
+ "execution_count": 1,
+ "id": "3",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "import pandas as pd\n",
+ "import numpy as np\n",
+ "import matplotlib.pyplot as plt"
+ ]
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "language": "python"
+ }
+ }
+}
diff --git a/vendor/gems/ipynbdiff/spec/testdata/only_code_no_kernelspec/expected.md b/vendor/gems/ipynbdiff/spec/testdata/only_code_no_kernelspec/expected.md
new file mode 100644
index 00000000000..9100045e0f5
--- /dev/null
+++ b/vendor/gems/ipynbdiff/spec/testdata/only_code_no_kernelspec/expected.md
@@ -0,0 +1,5 @@
+%% Cell type:code id:3 tags:
+
+```
+
+```
diff --git a/vendor/gems/ipynbdiff/spec/testdata/only_code_no_kernelspec/expected_symbols.txt b/vendor/gems/ipynbdiff/spec/testdata/only_code_no_kernelspec/expected_symbols.txt
new file mode 100644
index 00000000000..5f9ad320b8c
--- /dev/null
+++ b/vendor/gems/ipynbdiff/spec/testdata/only_code_no_kernelspec/expected_symbols.txt
@@ -0,0 +1,5 @@
+.cells.0
+
+.cells.0.source
+.cells.0.source
+
diff --git a/vendor/gems/ipynbdiff/spec/testdata/only_code_no_kernelspec/input.ipynb b/vendor/gems/ipynbdiff/spec/testdata/only_code_no_kernelspec/input.ipynb
new file mode 100644
index 00000000000..c3ff71057a6
--- /dev/null
+++ b/vendor/gems/ipynbdiff/spec/testdata/only_code_no_kernelspec/input.ipynb
@@ -0,0 +1,12 @@
+{
+ "cells": [
+ {
+ "cell_type": "code",
+ "execution_count": 1,
+ "id": "3",
+ "source": "",
+ "outputs": []
+ }
+ ],
+ "metadata": {}
+}
diff --git a/vendor/gems/ipynbdiff/spec/testdata/only_code_no_language/expected.md b/vendor/gems/ipynbdiff/spec/testdata/only_code_no_language/expected.md
new file mode 100644
index 00000000000..9100045e0f5
--- /dev/null
+++ b/vendor/gems/ipynbdiff/spec/testdata/only_code_no_language/expected.md
@@ -0,0 +1,5 @@
+%% Cell type:code id:3 tags:
+
+```
+
+```
diff --git a/vendor/gems/ipynbdiff/spec/testdata/only_code_no_language/expected_symbols.txt b/vendor/gems/ipynbdiff/spec/testdata/only_code_no_language/expected_symbols.txt
new file mode 100644
index 00000000000..5f9ad320b8c
--- /dev/null
+++ b/vendor/gems/ipynbdiff/spec/testdata/only_code_no_language/expected_symbols.txt
@@ -0,0 +1,5 @@
+.cells.0
+
+.cells.0.source
+.cells.0.source
+
diff --git a/vendor/gems/ipynbdiff/spec/testdata/only_code_no_language/input.ipynb b/vendor/gems/ipynbdiff/spec/testdata/only_code_no_language/input.ipynb
new file mode 100644
index 00000000000..fb16b106cbe
--- /dev/null
+++ b/vendor/gems/ipynbdiff/spec/testdata/only_code_no_language/input.ipynb
@@ -0,0 +1,14 @@
+{
+ "cells": [
+ {
+ "cell_type": "code",
+ "execution_count": 1,
+ "id": "3",
+ "source": "",
+ "outputs": []
+ }
+ ],
+ "metadata": {
+ "kernelspec": {}
+ }
+}
diff --git a/vendor/gems/ipynbdiff/spec/testdata/only_code_no_metadata/expected.md b/vendor/gems/ipynbdiff/spec/testdata/only_code_no_metadata/expected.md
new file mode 100644
index 00000000000..9100045e0f5
--- /dev/null
+++ b/vendor/gems/ipynbdiff/spec/testdata/only_code_no_metadata/expected.md
@@ -0,0 +1,5 @@
+%% Cell type:code id:3 tags:
+
+```
+
+```
diff --git a/vendor/gems/ipynbdiff/spec/testdata/only_code_no_metadata/expected_symbols.txt b/vendor/gems/ipynbdiff/spec/testdata/only_code_no_metadata/expected_symbols.txt
new file mode 100644
index 00000000000..5f9ad320b8c
--- /dev/null
+++ b/vendor/gems/ipynbdiff/spec/testdata/only_code_no_metadata/expected_symbols.txt
@@ -0,0 +1,5 @@
+.cells.0
+
+.cells.0.source
+.cells.0.source
+
diff --git a/vendor/gems/ipynbdiff/spec/testdata/only_code_no_metadata/input.ipynb b/vendor/gems/ipynbdiff/spec/testdata/only_code_no_metadata/input.ipynb
new file mode 100644
index 00000000000..364c080168b
--- /dev/null
+++ b/vendor/gems/ipynbdiff/spec/testdata/only_code_no_metadata/input.ipynb
@@ -0,0 +1,11 @@
+{
+ "cells": [
+ {
+ "cell_type": "code",
+ "execution_count": 1,
+ "id": "3",
+ "source": "",
+ "outputs": []
+ }
+ ]
+}
diff --git a/vendor/gems/ipynbdiff/spec/testdata/only_md/expected.md b/vendor/gems/ipynbdiff/spec/testdata/only_md/expected.md
new file mode 100644
index 00000000000..bdf4db5aea5
--- /dev/null
+++ b/vendor/gems/ipynbdiff/spec/testdata/only_md/expected.md
@@ -0,0 +1,5 @@
+%% Cell type:markdown id:1 tags:hello,world
+
+# A
+
+B
diff --git a/vendor/gems/ipynbdiff/spec/testdata/only_md/expected_symbols.txt b/vendor/gems/ipynbdiff/spec/testdata/only_md/expected_symbols.txt
new file mode 100644
index 00000000000..d3d6d526fc3
--- /dev/null
+++ b/vendor/gems/ipynbdiff/spec/testdata/only_md/expected_symbols.txt
@@ -0,0 +1,5 @@
+.cells.0
+
+.cells.0.source.0
+.cells.0.source.1
+.cells.0.source.2
diff --git a/vendor/gems/ipynbdiff/spec/testdata/only_md/input.ipynb b/vendor/gems/ipynbdiff/spec/testdata/only_md/input.ipynb
new file mode 100644
index 00000000000..9d6b550af31
--- /dev/null
+++ b/vendor/gems/ipynbdiff/spec/testdata/only_md/input.ipynb
@@ -0,0 +1,21 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "id": "1",
+ "metadata": {
+ "tags": [
+ "hello",
+ "world"
+ ]
+ },
+ "source": [
+ "# A\n",
+ "\n",
+ "B"
+ ]
+ }
+ ],
+ "metadata": {
+ }
+}
diff --git a/vendor/gems/ipynbdiff/spec/testdata/only_raw/expected.md b/vendor/gems/ipynbdiff/spec/testdata/only_raw/expected.md
new file mode 100644
index 00000000000..91c476e843b
--- /dev/null
+++ b/vendor/gems/ipynbdiff/spec/testdata/only_raw/expected.md
@@ -0,0 +1,4 @@
+%% Cell type:raw id:2 tags:
+
+A
+B
diff --git a/vendor/gems/ipynbdiff/spec/testdata/only_raw/expected_symbols.txt b/vendor/gems/ipynbdiff/spec/testdata/only_raw/expected_symbols.txt
new file mode 100644
index 00000000000..bceaf355c2f
--- /dev/null
+++ b/vendor/gems/ipynbdiff/spec/testdata/only_raw/expected_symbols.txt
@@ -0,0 +1,4 @@
+.cells.0
+
+.cells.0.source.0
+.cells.0.source.1
diff --git a/vendor/gems/ipynbdiff/spec/testdata/only_raw/input.ipynb b/vendor/gems/ipynbdiff/spec/testdata/only_raw/input.ipynb
new file mode 100644
index 00000000000..750e1bba615
--- /dev/null
+++ b/vendor/gems/ipynbdiff/spec/testdata/only_raw/input.ipynb
@@ -0,0 +1,15 @@
+{
+ "cells": [
+ {
+ "cell_type": "raw",
+ "id": "2",
+ "metadata": {},
+ "source": [
+ "A\n",
+ "B"
+ ]
+ }
+ ],
+ "metadata": {
+ }
+}
diff --git a/vendor/gems/ipynbdiff/spec/testdata/percent_decorator/expected.md b/vendor/gems/ipynbdiff/spec/testdata/percent_decorator/expected.md
new file mode 100644
index 00000000000..ecb0029f256
--- /dev/null
+++ b/vendor/gems/ipynbdiff/spec/testdata/percent_decorator/expected.md
@@ -0,0 +1,70 @@
+%% Cell type:markdown id:0aac5da7-745c-4eda-847a-3d0d07a1bb9b tags:
+
+# This is a markdown cell
+
+This paragraph has
+With
+Many
+Lines. How we will he handle MR notes?
+
+But I can add another paragraph
+
+%% Cell type:raw id:faecea5b-de0a-49fa-9a3a-61c2add652da tags:
+
+This is a raw cell
+With
+Multiple lines
+
+%% Cell type:code id:893ca2c0-ab75-4276-9dad-be1c40e16e8a tags:
+
+``` python
+import pandas as pd
+import numpy as np
+import matplotlib.pyplot as plt
+```
+
+%% Cell type:code id:0d707fb5-226f-46d6-80bd-489ebfb8905c tags:
+
+``` python
+np.random.seed(42)
+```
+
+%% Cell type:code id:35467fcf-28b1-4c7b-bb09-4cb192c35293 tags:senoid
+
+``` python
+x = np.linspace(0, 4*np.pi,50)
+y = np.sin(x)
+
+plt.plot(x, y)
+```
+
+%%%% Output: execute_result
+
+ [<matplotlib.lines.Line2D at 0x123e39370>]
+
+%%%% Output: display_data
+
+ ![](data:image/png;base64,some_invalid_base64_image_here)
+
+%% Cell type:code id:dc1178cd-c46d-4da3-9ab5-08f000699884 tags:
+
+``` python
+df = pd.DataFrame({"x": x, "y": y})
+```
+
+%% Cell type:code id:6e749b4f-b409-4700-870f-f68c39462490 tags:some-table
+
+``` python
+df[:2]
+```
+
+%%%% Output: execute_result
+
+ x y
+ 0 0.000000 0.000000
+ 1 0.256457 0.253655
+
+%% Cell type:code id:0ddef5ef-94a3-4afd-9c70-ddee9694f512 tags:
+
+``` python
+```
diff --git a/vendor/gems/ipynbdiff/spec/testdata/percent_decorator/expected_symbols.txt b/vendor/gems/ipynbdiff/spec/testdata/percent_decorator/expected_symbols.txt
new file mode 100644
index 00000000000..ab70e7bc908
--- /dev/null
+++ b/vendor/gems/ipynbdiff/spec/testdata/percent_decorator/expected_symbols.txt
@@ -0,0 +1,70 @@
+.cells.0
+
+.cells.0.source.0
+.cells.0.source.1
+.cells.0.source.2
+.cells.0.source.3
+.cells.0.source.4
+.cells.0.source.5
+.cells.0.source.6
+.cells.0.source.7
+
+.cells.1
+
+.cells.1.source.0
+.cells.1.source.1
+.cells.1.source.2
+
+.cells.2
+
+.cells.2.source
+.cells.2.source.0
+.cells.2.source.1
+.cells.2.source.2
+
+
+.cells.3
+
+.cells.3.source
+.cells.3.source.0
+
+
+.cells.4
+
+.cells.4.source
+.cells.4.source.0
+.cells.4.source.1
+.cells.4.source.2
+.cells.4.source.3
+
+
+.cells.4.outputs.0
+
+.cells.4.outputs.0.data.text/plain.0
+
+.cells.4.outputs.1
+
+.cells.4.outputs.1.data.image/png
+
+.cells.5
+
+.cells.5.source
+.cells.5.source.0
+
+
+.cells.6
+
+.cells.6.source
+.cells.6.source.0
+
+
+.cells.6.outputs.0
+
+.cells.6.outputs.0.data.text/plain.0
+.cells.6.outputs.0.data.text/plain.1
+.cells.6.outputs.0.data.text/plain.2
+
+.cells.7
+
+.cells.7.source
+
diff --git a/vendor/gems/ipynbdiff/spec/testdata/single_line_md/expected.md b/vendor/gems/ipynbdiff/spec/testdata/single_line_md/expected.md
new file mode 100644
index 00000000000..392a5048f59
--- /dev/null
+++ b/vendor/gems/ipynbdiff/spec/testdata/single_line_md/expected.md
@@ -0,0 +1,3 @@
+%% Cell type:markdown id:1 tags:hello,world
+
+A
diff --git a/vendor/gems/ipynbdiff/spec/testdata/single_line_md/expected_symbols.txt b/vendor/gems/ipynbdiff/spec/testdata/single_line_md/expected_symbols.txt
new file mode 100644
index 00000000000..86a7f6b3960
--- /dev/null
+++ b/vendor/gems/ipynbdiff/spec/testdata/single_line_md/expected_symbols.txt
@@ -0,0 +1,3 @@
+.cells.0
+
+.cells.0.source
diff --git a/vendor/gems/ipynbdiff/spec/testdata/single_line_md/input.ipynb b/vendor/gems/ipynbdiff/spec/testdata/single_line_md/input.ipynb
new file mode 100644
index 00000000000..5ebd41adbfa
--- /dev/null
+++ b/vendor/gems/ipynbdiff/spec/testdata/single_line_md/input.ipynb
@@ -0,0 +1,17 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "id": "1",
+ "metadata": {
+ "tags": [
+ "hello",
+ "world"
+ ]
+ },
+ "source": "A"
+ }
+ ],
+ "metadata": {
+ }
+}
diff --git a/vendor/gems/ipynbdiff/spec/testdata/stream_text/expected.md b/vendor/gems/ipynbdiff/spec/testdata/stream_text/expected.md
new file mode 100644
index 00000000000..fb862cbb636
--- /dev/null
+++ b/vendor/gems/ipynbdiff/spec/testdata/stream_text/expected.md
@@ -0,0 +1,9 @@
+%% Cell type:code id:123 tags:
+
+``` python
+print("G'bye")
+```
+
+%%%% Output: stream
+
+ G'bye
diff --git a/vendor/gems/ipynbdiff/spec/testdata/stream_text/expected_symbols.txt b/vendor/gems/ipynbdiff/spec/testdata/stream_text/expected_symbols.txt
new file mode 100644
index 00000000000..ed4a8a075d3
--- /dev/null
+++ b/vendor/gems/ipynbdiff/spec/testdata/stream_text/expected_symbols.txt
@@ -0,0 +1,9 @@
+.cells.0
+
+.cells.0.source
+.cells.0.source.0
+
+
+.cells.0.outputs.0
+
+.cells.0.outputs.0.text.0
diff --git a/vendor/gems/ipynbdiff/spec/testdata/stream_text/input.ipynb b/vendor/gems/ipynbdiff/spec/testdata/stream_text/input.ipynb
new file mode 100644
index 00000000000..14601fe35e5
--- /dev/null
+++ b/vendor/gems/ipynbdiff/spec/testdata/stream_text/input.ipynb
@@ -0,0 +1,27 @@
+{
+ "cells": [
+ {
+ "cell_type": "code",
+ "execution_count": 1,
+ "id": "123",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "G'bye\n"
+ ]
+ }
+ ],
+ "source": [
+ "print(\"G'bye\")"
+ ]
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "language": "python"
+ }
+ }
+}
diff --git a/vendor/gems/ipynbdiff/spec/testdata/svg/expected.md b/vendor/gems/ipynbdiff/spec/testdata/svg/expected.md
new file mode 100644
index 00000000000..37269446f5a
--- /dev/null
+++ b/vendor/gems/ipynbdiff/spec/testdata/svg/expected.md
@@ -0,0 +1,19 @@
+%% Cell type:code id:5 tags:
+
+``` python
+from IPython.display import SVG, display
+
+svg = """<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
+ <circle cx="50" cy="50" r="50"/>
+</svg>"""
+
+display(SVG(svg))
+```
+
+%%%% Output: display_data
+
+ ![](data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><circle cx="50" cy="50" r="50"/></svg>)
+
+%%%% Output: display_data
+
+ ![](data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><circle cx="50" cy="50" r="50"/></svg>)
diff --git a/vendor/gems/ipynbdiff/spec/testdata/svg/expected_symbols.txt b/vendor/gems/ipynbdiff/spec/testdata/svg/expected_symbols.txt
new file mode 100644
index 00000000000..dd2e412302d
--- /dev/null
+++ b/vendor/gems/ipynbdiff/spec/testdata/svg/expected_symbols.txt
@@ -0,0 +1,19 @@
+.cells.0
+
+.cells.0.source
+.cells.0.source.0
+.cells.0.source.1
+.cells.0.source.2
+.cells.0.source.3
+.cells.0.source.4
+.cells.0.source.5
+.cells.0.source.6
+
+
+.cells.0.outputs.0
+
+.cells.0.outputs.0.data.image/svg+xml
+
+.cells.0.outputs.1
+
+.cells.0.outputs.1.data.image/svg+xml
diff --git a/vendor/gems/ipynbdiff/spec/testdata/svg/input.ipynb b/vendor/gems/ipynbdiff/spec/testdata/svg/input.ipynb
new file mode 100644
index 00000000000..a02d01f7bf2
--- /dev/null
+++ b/vendor/gems/ipynbdiff/spec/testdata/svg/input.ipynb
@@ -0,0 +1,66 @@
+{
+ "cells": [
+ {
+ "cell_type": "code",
+ "execution_count": 10,
+ "id": "5",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "image/svg+xml": [
+ "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 100 100\">\n",
+ " <circle cx=\"50\" cy=\"50\" r=\"50\"/>\n",
+ "</svg>"
+ ],
+ "text/plain": [
+ "<IPython.core.display.SVG object>"
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ },
+ {
+ "data": {
+ "image/svg+xml": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 100 100\"><circle cx=\"50\" cy=\"50\" r=\"50\"/></svg>",
+ "text/plain": [
+ "<IPython.core.display.SVG object>"
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "from IPython.display import SVG, display\n",
+ "\n",
+ "svg = \"\"\"<svg viewBox=\"0 0 100 100\" xmlns=\"http://www.w3.org/2000/svg\">\n",
+ " <circle cx=\"50\" cy=\"50\" r=\"50\"/>\n",
+ "</svg>\"\"\"\n",
+ "\n",
+ "display(SVG(svg))"
+ ]
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": "Python 3 (ipykernel)",
+ "language": "python",
+ "name": "python3"
+ },
+ "language_info": {
+ "codemirror_mode": {
+ "name": "ipython",
+ "version": 3
+ },
+ "file_extension": ".py",
+ "mimetype": "text/x-python",
+ "name": "python",
+ "nbconvert_exporter": "python",
+ "pygments_lexer": "ipython3",
+ "version": "3.8.12"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 5
+}
diff --git a/vendor/gems/ipynbdiff/spec/testdata/text_output/expected.md b/vendor/gems/ipynbdiff/spec/testdata/text_output/expected.md
new file mode 100644
index 00000000000..924f4939f54
--- /dev/null
+++ b/vendor/gems/ipynbdiff/spec/testdata/text_output/expected.md
@@ -0,0 +1,9 @@
+%% Cell type:code id:5 tags:senoid
+
+``` python
+plt.plot(x, y)
+```
+
+%%%% Output: execute_result
+
+ [<matplotlib.lines.Line2D at 0x12a4e43d0>]
diff --git a/vendor/gems/ipynbdiff/spec/testdata/text_output/expected_symbols.txt b/vendor/gems/ipynbdiff/spec/testdata/text_output/expected_symbols.txt
new file mode 100644
index 00000000000..179b30098a1
--- /dev/null
+++ b/vendor/gems/ipynbdiff/spec/testdata/text_output/expected_symbols.txt
@@ -0,0 +1,9 @@
+.cells.0
+
+.cells.0.source
+.cells.0.source.0
+
+
+.cells.0.outputs.0
+
+.cells.0.outputs.0.data.text/plain.0
diff --git a/vendor/gems/ipynbdiff/spec/testdata/text_output/input.ipynb b/vendor/gems/ipynbdiff/spec/testdata/text_output/input.ipynb
new file mode 100644
index 00000000000..b1b387bb99d
--- /dev/null
+++ b/vendor/gems/ipynbdiff/spec/testdata/text_output/input.ipynb
@@ -0,0 +1,31 @@
+{
+ "cells": [
+ {
+ "cell_type": "code",
+ "id": "5",
+ "metadata": {
+ "tags": [
+ "senoid"
+ ]
+ },
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "[<matplotlib.lines.Line2D at 0x12a4e43d0>]"
+ ]
+ },
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "plt.plot(x, y)"
+ ]
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "language": "python"
+ }
+ }
+}
diff --git a/vendor/gems/ipynbdiff/spec/testdata/text_png_output/expected.md b/vendor/gems/ipynbdiff/spec/testdata/text_png_output/expected.md
new file mode 100644
index 00000000000..b1dda235951
--- /dev/null
+++ b/vendor/gems/ipynbdiff/spec/testdata/text_png_output/expected.md
@@ -0,0 +1,16 @@
+%% Cell type:code id:5 tags:senoid
+
+``` python
+x = np.linspace(0, 4*np.pi,50)
+y = 2 * np.sin(x)
+
+plt.plot(x, y)
+```
+
+%%%% Output: execute_result
+
+ [<matplotlib.lines.Line2D at 0x12a4e43d0>]
+
+%%%% Output: display_data
+
+ ![](data:image/png;base64,this_is_an_invalid_hash_for_testing_purposes)
diff --git a/vendor/gems/ipynbdiff/spec/testdata/text_png_output/expected_symbols.txt b/vendor/gems/ipynbdiff/spec/testdata/text_png_output/expected_symbols.txt
new file mode 100644
index 00000000000..5a86e4daa67
--- /dev/null
+++ b/vendor/gems/ipynbdiff/spec/testdata/text_png_output/expected_symbols.txt
@@ -0,0 +1,16 @@
+.cells.0
+
+.cells.0.source
+.cells.0.source.0
+.cells.0.source.1
+.cells.0.source.2
+.cells.0.source.3
+
+
+.cells.0.outputs.0
+
+.cells.0.outputs.0.data.text/plain.0
+
+.cells.0.outputs.1
+
+.cells.0.outputs.1.data.image/png
diff --git a/vendor/gems/ipynbdiff/spec/testdata/text_png_output/input.ipynb b/vendor/gems/ipynbdiff/spec/testdata/text_png_output/input.ipynb
new file mode 100644
index 00000000000..3728b129d26
--- /dev/null
+++ b/vendor/gems/ipynbdiff/spec/testdata/text_png_output/input.ipynb
@@ -0,0 +1,49 @@
+{
+ "cells": [
+ {
+ "cell_type": "code",
+ "execution_count": 3,
+ "id": "5",
+ "metadata": {
+ "tags": [
+ "senoid"
+ ]
+ },
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "[<matplotlib.lines.Line2D at 0x12a4e43d0>]"
+ ]
+ },
+ "execution_count": 3,
+ "metadata": {},
+ "output_type": "execute_result"
+ },
+ {
+ "data": {
+ "image/png": "this_is_an_invalid_hash_for_testing_purposes\n",
+ "text/plain": [
+ "<Figure size 432x288 with 1 Axes>"
+ ]
+ },
+ "metadata": {
+ "needs_background": "light"
+ },
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "x = np.linspace(0, 4*np.pi,50)\n",
+ "y = 2 * np.sin(x)\n",
+ "\n",
+ "plt.plot(x, y)"
+ ]
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "language": "python"
+ }
+ }
+}
diff --git a/vendor/gems/ipynbdiff/spec/testdata/to.ipynb b/vendor/gems/ipynbdiff/spec/testdata/to.ipynb
new file mode 100644
index 00000000000..99b51f3b857
--- /dev/null
+++ b/vendor/gems/ipynbdiff/spec/testdata/to.ipynb
@@ -0,0 +1,200 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "id": "0aac5da7-745c-4eda-847a-3d0d07a1bb9b",
+ "metadata": {
+ "tags": []
+ },
+ "source": [
+ "# This is a markdown cell\n",
+ "\n",
+ "This paragraph has\n",
+ "With\n",
+ "Many\n",
+ "Lines. How we will he handle MR notes?\n",
+ "\n",
+ "But I can add another paragraph\n",
+ "\n",
+ "Another paragraph added"
+ ]
+ },
+ {
+ "cell_type": "raw",
+ "id": "faecea5b-de0a-49fa-9a3a-61c2add652da",
+ "metadata": {},
+ "source": [
+ "This is a raw cell\n",
+ "With\n",
+ "Multiple lines"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 1,
+ "id": "893ca2c0-ab75-4276-9dad-be1c40e16e8a",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "import pandas as pd\n",
+ "import numpy as np\n",
+ "import matplotlib.pyplot as plt"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 2,
+ "id": "0d707fb5-226f-46d6-80bd-489ebfb8905c",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "np.random.seed(42)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 3,
+ "id": "35467fcf-28b1-4c7b-bb09-4cb192c35293",
+ "metadata": {
+ "tags": [
+ "senoid"
+ ]
+ },
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "[<matplotlib.lines.Line2D at 0x12a4e43d0>]"
+ ]
+ },
+ "execution_count": 3,
+ "metadata": {},
+ "output_type": "execute_result"
+ },
+ {
+ "data": {
+ "image/png": "another_invalid_base64_image_here\n",
+ "text/plain": [
+ "<Figure size 432x288 with 1 Axes>"
+ ]
+ },
+ "metadata": {
+ "needs_background": "light"
+ },
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "x = np.linspace(0, 4*np.pi,50)\n",
+ "y = 2 * np.sin(x)\n",
+ "\n",
+ "plt.plot(x, y)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 4,
+ "id": "dc1178cd-c46d-4da3-9ab5-08f000699884",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "df = pd.DataFrame({\"x\": x, \"y\": y})"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 5,
+ "id": "6e749b4f-b409-4700-870f-f68c39462490",
+ "metadata": {
+ "tags": [
+ "some-table"
+ ]
+ },
+ "outputs": [
+ {
+ "data": {
+ "text/html": [
+ "<div>\n",
+ "<style scoped>\n",
+ " .dataframe tbody tr th:only-of-type {\n",
+ " vertical-align: middle;\n",
+ " }\n",
+ "\n",
+ " .dataframe tbody tr th {\n",
+ " vertical-align: top;\n",
+ " }\n",
+ "\n",
+ " .dataframe thead th {\n",
+ " text-align: right;\n",
+ " }\n",
+ "</style>\n",
+ "<table border=\"1\" class=\"dataframe\">\n",
+ " <thead>\n",
+ " <tr style=\"text-align: right;\">\n",
+ " <th></th>\n",
+ " <th>x</th>\n",
+ " <th>y</th>\n",
+ " </tr>\n",
+ " </thead>\n",
+ " <tbody>\n",
+ " <tr>\n",
+ " <th>0</th>\n",
+ " <td>0.000000</td>\n",
+ " <td>0.000000</td>\n",
+ " </tr>\n",
+ " <tr>\n",
+ " <th>1</th>\n",
+ " <td>0.256457</td>\n",
+ " <td>0.507309</td>\n",
+ " </tr>\n",
+ " </tbody>\n",
+ "</table>\n",
+ "</div>"
+ ],
+ "text/plain": [
+ " x y\n",
+ "0 0.000000 0.000000\n",
+ "1 0.256457 0.507309"
+ ]
+ },
+ "execution_count": 5,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "df[:2]"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "0ddef5ef-94a3-4afd-9c70-ddee9694f512",
+ "metadata": {},
+ "outputs": [],
+ "source": []
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": "New Python 3 (ipykernel)",
+ "language": "python",
+ "name": "python3"
+ },
+ "language_info": {
+ "codemirror_mode": {
+ "name": "ipython",
+ "version": 3
+ },
+ "file_extension": ".py",
+ "mimetype": "text/x-python",
+ "name": "python",
+ "nbconvert_exporter": "python",
+ "pygments_lexer": "ipython3",
+ "version": "3.9.7"
+ },
+ "toc-showtags": true
+ },
+ "nbformat": 4,
+ "nbformat_minor": 5
+}
diff --git a/vendor/gems/ipynbdiff/spec/testdata/unknown_output_type/expected.md b/vendor/gems/ipynbdiff/spec/testdata/unknown_output_type/expected.md
new file mode 100644
index 00000000000..af34d6eb8c3
--- /dev/null
+++ b/vendor/gems/ipynbdiff/spec/testdata/unknown_output_type/expected.md
@@ -0,0 +1,5 @@
+%% Cell type:code id:123 tags:
+
+``` python
+print("G'bye")
+```
diff --git a/vendor/gems/ipynbdiff/spec/testdata/unknown_output_type/expected_symbols.txt b/vendor/gems/ipynbdiff/spec/testdata/unknown_output_type/expected_symbols.txt
new file mode 100644
index 00000000000..cb35f88c897
--- /dev/null
+++ b/vendor/gems/ipynbdiff/spec/testdata/unknown_output_type/expected_symbols.txt
@@ -0,0 +1,5 @@
+.cells.0
+
+.cells.0.source
+.cells.0.source.0
+
diff --git a/vendor/gems/ipynbdiff/spec/testdata/unknown_output_type/input.ipynb b/vendor/gems/ipynbdiff/spec/testdata/unknown_output_type/input.ipynb
new file mode 100644
index 00000000000..42f4b39b365
--- /dev/null
+++ b/vendor/gems/ipynbdiff/spec/testdata/unknown_output_type/input.ipynb
@@ -0,0 +1,27 @@
+{
+ "cells": [
+ {
+ "cell_type": "code",
+ "execution_count": 1,
+ "id": "123",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "unknown_output",
+ "text": [
+ "G'bye\n"
+ ]
+ }
+ ],
+ "source": [
+ "print(\"G'bye\")"
+ ]
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "language": "python"
+ }
+ }
+}
diff --git a/vendor/gems/ipynbdiff/spec/transformer_spec.rb b/vendor/gems/ipynbdiff/spec/transformer_spec.rb
new file mode 100644
index 00000000000..8f9527847fa
--- /dev/null
+++ b/vendor/gems/ipynbdiff/spec/transformer_spec.rb
@@ -0,0 +1,90 @@
+# frozen_string_literal: true
+
+require 'rspec'
+require 'ipynbdiff'
+require 'json'
+require 'rspec-parameterized'
+
+BASE_PATH = File.join(File.expand_path(File.dirname(__FILE__)), 'testdata')
+
+def read_file(*paths)
+ File.read(File.join(BASE_PATH, *paths))
+end
+
+def default_config
+ @default_config ||= {
+ include_frontmatter: false,
+ hide_images: false
+ }
+end
+
+def from_ipynb
+ @from_ipynb ||= read_file('from.ipynb')
+end
+
+def read_notebook(input_path)
+ read_file(input_path, 'input.ipynb')
+rescue Errno::ENOENT
+ from_ipynb
+end
+
+describe IpynbDiff::Transformer do
+ describe 'When notebook is valid' do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:ctx, :test_case, :config) do
+ 'renders metadata' | 'no_cells' | { include_frontmatter: true }
+ 'is empty for no cells, but metadata is false' | 'no_cells_no_metadata' | {}
+ 'adds markdown cell' | 'only_md' | {}
+ 'adds block with only one line of markdown' | 'single_line_md' | {}
+ 'adds raw block' | 'only_raw' | {}
+ 'code cell, but no output' | 'only_code' | {}
+ 'code cell, but no language' | 'only_code_no_language' | {}
+ 'code cell, but no kernelspec' | 'only_code_no_kernelspec' | {}
+ 'code cell, but no nb metadata' | 'only_code_no_metadata' | {}
+ 'text output' | 'text_output' | {}
+ 'ignores html output' | 'ignore_html_output' | {}
+ 'extracts png output along with text' | 'text_png_output' | {}
+ 'embeds svg as image' | 'svg' | {}
+ 'extracts latex output' | 'latex_output' | {}
+ 'extracts error output' | 'error_output' | {}
+ 'does not fetch tags if there is no cell metadata' | 'no_metadata_on_cell' | {}
+ 'generates :percent decorator' | 'percent_decorator' | {}
+ 'parses stream output' | 'stream_text' | {}
+ 'ignores unknown output type' | 'unknown_output_type' | {}
+ 'handles backslash correctly' | 'backslash_as_last_char' | {}
+ 'multiline png output' | 'multiline_png_output' | {}
+ 'hides images when option passed' | 'hide_images' | { hide_images: true }
+ end
+
+ with_them do
+ let(:expected_md) { read_file(test_case, 'expected.md') }
+ let(:expected_symbols) { read_file(test_case, 'expected_symbols.txt') }
+ let(:input) { read_notebook(test_case) }
+ let(:transformed) { IpynbDiff::Transformer.new(**default_config.merge(config)).transform(input) }
+
+ it 'generates the expected markdown' do
+ expect(transformed.as_text).to eq expected_md
+ end
+
+ it 'generates the expected symbol map' do
+ expect(transformed.blocks.map { |b| b[:source_symbol] }.join("\n")).to eq expected_symbols
+ end
+ end
+ end
+
+ context 'When the notebook is invalid' do
+ [
+ ['because the json is invalid', 'a'],
+ ['because it doesnt have the cell tag', '{"metadata":[]}']
+ ].each do |ctx, notebook|
+ context ctx do
+ it 'raises error' do
+ expect do
+ IpynbDiff::Transformer.new.transform(notebook)
+ end.to raise_error(IpynbDiff::InvalidNotebookError)
+ end
+ end
+ end
+ end
+end