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:
authorGitLab Bot <gitlab-bot@gitlab.com>2022-06-02 03:09:20 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2022-06-02 03:09:20 +0300
commite35ac5e805fcb47d43591f605a9d5abfb75f44b8 (patch)
treefca5b55f47b18ade0891c322b14eb50baf0a44ef /app/assets/javascripts/content_editor
parente2ef50dafcf51e811123dd71179334de2ea3edf9 (diff)
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app/assets/javascripts/content_editor')
-rw-r--r--app/assets/javascripts/content_editor/extensions/sourcemap.js4
-rw-r--r--app/assets/javascripts/content_editor/services/hast_to_prosemirror_converter.js36
-rw-r--r--app/assets/javascripts/content_editor/services/markdown_serializer.js13
-rw-r--r--app/assets/javascripts/content_editor/services/remark_markdown_deserializer.js54
-rw-r--r--app/assets/javascripts/content_editor/services/serialization_helpers.js78
5 files changed, 151 insertions, 34 deletions
diff --git a/app/assets/javascripts/content_editor/extensions/sourcemap.js b/app/assets/javascripts/content_editor/extensions/sourcemap.js
index 61d1d983846..1cc60e33e13 100644
--- a/app/assets/javascripts/content_editor/extensions/sourcemap.js
+++ b/app/assets/javascripts/content_editor/extensions/sourcemap.js
@@ -14,6 +14,8 @@ import ListItem from './list_item';
import OrderedList from './ordered_list';
import Paragraph from './paragraph';
import Strike from './strike';
+import TaskList from './task_list';
+import TaskItem from './task_item';
export default Extension.create({
addGlobalAttributes() {
@@ -35,6 +37,8 @@ export default Extension.create({
OrderedList.name,
Paragraph.name,
Strike.name,
+ TaskList.name,
+ TaskItem.name,
],
attributes: {
sourceMarkdown: {
diff --git a/app/assets/javascripts/content_editor/services/hast_to_prosemirror_converter.js b/app/assets/javascripts/content_editor/services/hast_to_prosemirror_converter.js
index 9f8505416ba..92faaabac0d 100644
--- a/app/assets/javascripts/content_editor/services/hast_to_prosemirror_converter.js
+++ b/app/assets/javascripts/content_editor/services/hast_to_prosemirror_converter.js
@@ -22,7 +22,7 @@
import { Mark } from 'prosemirror-model';
import { visitParents } from 'unist-util-visit-parents';
import { toString } from 'hast-util-to-string';
-import { isFunction } from 'lodash';
+import { isFunction, noop } from 'lodash';
/**
* Merges two ProseMirror text nodes if both text nodes
@@ -268,17 +268,19 @@ const createProseMirrorNodeFactories = (schema, proseMirrorFactorySpecs, source)
selector: 'text',
handle: (state, hastNode, parent) => {
const { factorySpec } = state.top;
+ const { processText, wrapTextInParagraph } = factorySpec;
+ const { value: text } = hastNode;
- if (/^\s+$/.test(hastNode.value)) {
+ if (/^\s+$/.test(text)) {
return;
}
- if (factorySpec.wrapTextInParagraph === true) {
+ if (wrapTextInParagraph === true) {
state.openNode(schema.nodeType('paragraph'), hastNode, getAttrs({}, parent, [], source));
- state.addText(schema, hastNode.value);
+ state.addText(schema, isFunction(processText) ? processText(text) : text);
state.closeNode();
} else {
- state.addText(schema, hastNode.value);
+ state.addText(schema, text);
}
},
},
@@ -287,6 +289,7 @@ const createProseMirrorNodeFactories = (schema, proseMirrorFactorySpecs, source)
const factory = {
selector: factorySpec.selector,
skipChildren: factorySpec.skipChildren,
+ processText: factorySpec.processText,
};
if (factorySpec.type === 'block') {
@@ -335,6 +338,8 @@ const createProseMirrorNodeFactories = (schema, proseMirrorFactorySpecs, source)
state.addText(schema, hastNode.value);
}
};
+ } else if (factorySpec.type === 'ignore') {
+ factory.handle = noop;
} else {
throw new RangeError(
`Unrecognized ProseMirror object type ${JSON.stringify(factorySpec.type)}`,
@@ -347,12 +352,12 @@ const createProseMirrorNodeFactories = (schema, proseMirrorFactorySpecs, source)
return factories;
};
-const findFactory = (hastNode, factories) =>
+const findFactory = (hastNode, ancestors, factories) =>
Object.entries(factories).find(([, factorySpec]) => {
const { selector } = factorySpec;
return isFunction(selector)
- ? selector(hastNode)
+ ? selector(hastNode, ancestors)
: [hastNode.tagName, hastNode.type].includes(selector);
})?.[1];
@@ -408,6 +413,8 @@ const findFactory = (hastNode, factories) =>
* 2. "inline": A ProseMirror node that doesn’t contain any children although
* it can have inline content like an image or a mention object.
* 3. "mark": A ProseMirror mark.
+ * 4. "ignore": A hast node that should be ignored and won’t be mapped to a
+ * ProseMirror node.
*
* **selector**
*
@@ -417,8 +424,8 @@ const findFactory = (hastNode, factories) =>
* that equals the string value.
*
* If you assign a function, the converter will invoke the function with
- * the hast node. The function should return `true` if the hastNode matches
- * the custom criteria implemented in the function
+ * the hast node and its ancestors. The function should return `true`
+ * if the hastNode matches the custom criteria implemented in the function
*
* **getAttrs**
*
@@ -435,6 +442,12 @@ const findFactory = (hastNode, factories) =>
* it will wrap that text in a paragraph. This is useful for ProseMirror block
* nodes that don’t allow text directly such as list items and tables.
*
+ * **processText**
+ *
+ * This property only applies to block nodes. If a block node contains text,
+ * it allows applying a processing function to that text. This is useful when
+ * you can transform the text node, i.e trim(), substring(), etc.
+ *
* **skipChildren**
*
* Skips a hast node’s children while traversing the tree.
@@ -461,7 +474,8 @@ export const createProseMirrorDocFromMdastTree = ({ schema, factorySpecs, tree,
const state = new HastToProseMirrorConverterState();
visitParents(tree, (hastNode, ancestors) => {
- const factory = findFactory(hastNode, proseMirrorNodeFactories);
+ const factory = findFactory(hastNode, ancestors, proseMirrorNodeFactories);
+ const parent = ancestors[ancestors.length - 1];
if (!factory) {
throw new Error(
@@ -471,8 +485,6 @@ export const createProseMirrorDocFromMdastTree = ({ schema, factorySpecs, tree,
);
}
- const parent = ancestors[ancestors.length - 1];
-
factory.handle(state, hastNode, parent);
return factory.skipChildren === true ? 'skip' : true;
diff --git a/app/assets/javascripts/content_editor/services/markdown_serializer.js b/app/assets/javascripts/content_editor/services/markdown_serializer.js
index cd3ac6fcb1f..dccc545f6cb 100644
--- a/app/assets/javascripts/content_editor/services/markdown_serializer.js
+++ b/app/assets/javascripts/content_editor/services/markdown_serializer.js
@@ -60,6 +60,7 @@ import {
renderPlayable,
renderHTMLNode,
renderContent,
+ renderBulletList,
preserveUnchanged,
bold,
italic,
@@ -120,7 +121,7 @@ const defaultSerializerConfig = {
state.wrapBlock('> ', null, node, () => state.renderContent(node));
}
}),
- [BulletList.name]: preserveUnchanged(defaultMarkdownSerializer.nodes.bullet_list),
+ [BulletList.name]: preserveUnchanged(renderBulletList),
[CodeBlockHighlight.name]: preserveUnchanged(renderCodeBlock),
[Diagram.name]: renderCodeBlock,
[Division.name]: (state, node) => {
@@ -196,14 +197,14 @@ const defaultSerializerConfig = {
[TableCell.name]: renderTableCell,
[TableHeader.name]: renderTableCell,
[TableRow.name]: renderTableRow,
- [TaskItem.name]: (state, node) => {
+ [TaskItem.name]: preserveUnchanged((state, node) => {
state.write(`[${node.attrs.checked ? 'x' : ' '}] `);
state.renderContent(node);
- },
- [TaskList.name]: (state, node) => {
+ }),
+ [TaskList.name]: preserveUnchanged((state, node) => {
if (node.attrs.numeric) renderOrderedList(state, node);
- else defaultMarkdownSerializer.nodes.bullet_list(state, node);
- },
+ else renderBulletList(state, node);
+ }),
[Text.name]: defaultMarkdownSerializer.nodes.text,
[Video.name]: renderPlayable,
[WordBreak.name]: (state) => state.write('<wbr>'),
diff --git a/app/assets/javascripts/content_editor/services/remark_markdown_deserializer.js b/app/assets/javascripts/content_editor/services/remark_markdown_deserializer.js
index 4cbbfc36151..bec18bcdd81 100644
--- a/app/assets/javascripts/content_editor/services/remark_markdown_deserializer.js
+++ b/app/assets/javascripts/content_editor/services/remark_markdown_deserializer.js
@@ -2,12 +2,31 @@ import { isString } from 'lodash';
import { render } from '~/lib/gfm';
import { createProseMirrorDocFromMdastTree } from './hast_to_prosemirror_converter';
+const isTaskItem = (hastNode) => {
+ const { className } = hastNode.properties;
+
+ return (
+ hastNode.tagName === 'li' && Array.isArray(className) && className.includes('task-list-item')
+ );
+};
+
const factorySpecs = {
blockquote: { type: 'block', selector: 'blockquote' },
paragraph: { type: 'block', selector: 'p' },
- listItem: { type: 'block', selector: 'li', wrapTextInParagraph: true },
- orderedList: { type: 'block', selector: 'ol' },
- bulletList: { type: 'block', selector: 'ul' },
+ listItem: {
+ type: 'block',
+ wrapTextInParagraph: true,
+ processText: (text) => text.trim(),
+ selector: (hastNode) => hastNode.tagName === 'li' && !hastNode.properties.className,
+ },
+ orderedList: {
+ type: 'block',
+ selector: (hastNode) => hastNode.tagName === 'ol' && !hastNode.properties.className,
+ },
+ bulletList: {
+ type: 'block',
+ selector: (hastNode) => hastNode.tagName === 'ul' && !hastNode.properties.className,
+ },
heading: {
type: 'block',
selector: (hastNode) => ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'].includes(hastNode.tagName),
@@ -33,6 +52,35 @@ const factorySpecs = {
type: 'block',
selector: 'hr',
},
+ taskList: {
+ type: 'block',
+ selector: (hastNode) => {
+ const { className } = hastNode.properties;
+
+ return (
+ ['ul', 'ol'].includes(hastNode.tagName) &&
+ Array.isArray(className) &&
+ className.includes('contains-task-list')
+ );
+ },
+ getAttrs: (hastNode) => ({
+ numeric: hastNode.tagName === 'ol',
+ }),
+ },
+ taskItem: {
+ type: 'block',
+ wrapTextInParagraph: true,
+ processText: (text) => text.trim(),
+ selector: isTaskItem,
+ getAttrs: (hastNode) => ({
+ checked: hastNode.children[0].properties.checked,
+ }),
+ },
+ taskItemCheckbox: {
+ type: 'ignore',
+ selector: (hastNode, ancestors) =>
+ hastNode.tagName === 'input' && isTaskItem(ancestors[ancestors.length - 1]),
+ },
image: {
type: 'inline',
selector: 'img',
diff --git a/app/assets/javascripts/content_editor/services/serialization_helpers.js b/app/assets/javascripts/content_editor/services/serialization_helpers.js
index c11ce08de63..4019b772269 100644
--- a/app/assets/javascripts/content_editor/services/serialization_helpers.js
+++ b/app/assets/javascripts/content_editor/services/serialization_helpers.js
@@ -272,19 +272,6 @@ export function renderHTMLNode(tagName, forceRenderContentInline = false) {
};
}
-export function renderOrderedList(state, node) {
- const { parens } = node.attrs;
- const start = node.attrs.start || 1;
- const maxW = String(start + node.childCount - 1).length;
- const space = state.repeat(' ', maxW + 2);
- const delimiter = parens ? ')' : '.';
-
- state.renderList(node, space, (i) => {
- const nStr = String(start + i);
- return `${state.repeat(' ', maxW - nStr.length) + nStr}${delimiter} `;
- });
-}
-
export function renderTableCell(state, node) {
if (!isInBlockTable(node) || containsParagraphWithOnlyText(node)) {
state.renderInline(node.child(0));
@@ -364,6 +351,71 @@ export function preserveUnchanged(render) {
};
}
+/**
+ * We extracted this function from
+ * https://github.com/ProseMirror/prosemirror-markdown/blob/master/src/to_markdown.ts#L350.
+ *
+ * We need to overwrite this function because we don’t want to wrap the list item nodes
+ * with the bullet delimiter when the list item node hasn’t changed
+ */
+const renderList = (state, node, delim, firstDelim) => {
+ if (state.closed && state.closed.type === node.type) state.flushClose(3);
+ else if (state.inTightList) state.flushClose(1);
+
+ const isTight =
+ typeof node.attrs.tight !== 'undefined' ? node.attrs.tight : state.options.tightLists;
+ const prevTight = state.inTightList;
+
+ state.inTightList = isTight;
+
+ node.forEach((child, _, i) => {
+ const same = state.options.changeTracker.get(child);
+
+ if (i && isTight) {
+ state.flushClose(1);
+ }
+
+ if (same) {
+ // Avoid wrapping list item when node hasn’t changed
+ state.render(child, node, i);
+ } else {
+ state.wrapBlock(delim, firstDelim(i), node, () => state.render(child, node, i));
+ }
+ });
+
+ state.inTightList = prevTight;
+};
+
+export const renderBulletList = (state, node) => {
+ const { sourceMarkdown, bullet: bulletAttr } = node.attrs;
+ const bullet = /^(\*|\+|-)\s/.exec(sourceMarkdown)?.[1] || bulletAttr || '*';
+
+ renderList(state, node, ' ', () => `${bullet} `);
+};
+
+export function renderOrderedList(state, node) {
+ const { sourceMarkdown } = node.attrs;
+ let start;
+ let delimiter;
+
+ if (sourceMarkdown) {
+ const match = /^(\d+)(\)|\.)/.exec(sourceMarkdown);
+ start = parseInt(match[1], 10) || 1;
+ [, , delimiter] = match;
+ } else {
+ start = node.attrs.start || 1;
+ delimiter = node.attrs.parens ? ')' : '.';
+ }
+
+ const maxW = String(start + node.childCount - 1).length;
+ const space = state.repeat(' ', maxW + 2);
+
+ renderList(state, node, space, (i) => {
+ const nStr = String(start + i);
+ return `${state.repeat(' ', maxW - nStr.length) + nStr}${delimiter} `;
+ });
+}
+
const generateBoldTags = (wrapTagName = openTag) => {
return (_, mark) => {
const type = /^(\*\*|__|<strong|<b).*/.exec(mark.attrs.sourceMarkdown)?.[1];