Merge branch 'db-copy-as-gfm-prosemirror' into 'master'
Reimplement Copy-as-GFM using the prosemirror document model See merge request gitlab-org/gitlab-ce!22797
This commit is contained in:
commit
cc29bc61e0
|
@ -1,320 +1,8 @@
|
|||
/* eslint-disable object-shorthand, no-unused-vars, no-use-before-define, no-restricted-syntax, guard-for-in, no-continue */
|
||||
|
||||
import $ from 'jquery';
|
||||
import _ from 'underscore';
|
||||
import { insertText, getSelectedFragment, nodeMatchesSelector } from '~/lib/utils/common_utils';
|
||||
import { placeholderImage } from '~/lazy_loader';
|
||||
|
||||
const gfmRules = {
|
||||
// The filters referenced in lib/banzai/pipeline/gfm_pipeline.rb convert
|
||||
// GitLab Flavored Markdown (GFM) to HTML.
|
||||
// These handlers consequently convert that same HTML to GFM to be copied to the clipboard.
|
||||
// Every filter in lib/banzai/pipeline/gfm_pipeline.rb that generates HTML
|
||||
// from GFM should have a handler here, in reverse order.
|
||||
// The GFM-to-HTML-to-GFM cycle is tested in spec/features/copy_as_gfm_spec.rb.
|
||||
InlineDiffFilter: {
|
||||
'span.idiff.addition'(el, text) {
|
||||
return `{+${text}+}`;
|
||||
},
|
||||
'span.idiff.deletion'(el, text) {
|
||||
return `{-${text}-}`;
|
||||
},
|
||||
},
|
||||
TaskListFilter: {
|
||||
'input[type=checkbox].task-list-item-checkbox'(el) {
|
||||
return `[${el.checked ? 'x' : ' '}]`;
|
||||
},
|
||||
},
|
||||
ReferenceFilter: {
|
||||
'.tooltip'(el) {
|
||||
return '';
|
||||
},
|
||||
'a.gfm:not([data-link=true])'(el, text) {
|
||||
return el.dataset.original || text;
|
||||
},
|
||||
},
|
||||
AutolinkFilter: {
|
||||
a(el, text) {
|
||||
// Fallback on the regular MarkdownFilter's `a` handler.
|
||||
if (text !== el.getAttribute('href')) return false;
|
||||
|
||||
return text;
|
||||
},
|
||||
},
|
||||
TableOfContentsFilter: {
|
||||
'ul.section-nav'(el) {
|
||||
return '[[_TOC_]]';
|
||||
},
|
||||
},
|
||||
EmojiFilter: {
|
||||
'img.emoji'(el) {
|
||||
return el.getAttribute('alt');
|
||||
},
|
||||
'gl-emoji'(el) {
|
||||
return `:${el.getAttribute('data-name')}:`;
|
||||
},
|
||||
},
|
||||
ImageLinkFilter: {
|
||||
'a.no-attachment-icon'(el, text) {
|
||||
return text;
|
||||
},
|
||||
},
|
||||
ImageLazyLoadFilter: {
|
||||
img(el, text) {
|
||||
return `![${el.getAttribute('alt')}](${el.getAttribute('src')})`;
|
||||
},
|
||||
},
|
||||
VideoLinkFilter: {
|
||||
'.video-container'(el) {
|
||||
const videoEl = el.querySelector('video');
|
||||
if (!videoEl) return false;
|
||||
|
||||
return CopyAsGFM.nodeToGFM(videoEl);
|
||||
},
|
||||
video(el) {
|
||||
return `![${el.dataset.title}](${el.getAttribute('src')})`;
|
||||
},
|
||||
},
|
||||
MermaidFilter: {
|
||||
'svg.mermaid'(el, text) {
|
||||
const sourceEl = el.querySelector('text.source');
|
||||
if (!sourceEl) return false;
|
||||
|
||||
return `\`\`\`mermaid\n${CopyAsGFM.nodeToGFM(sourceEl)}\n\`\`\``;
|
||||
},
|
||||
'svg.mermaid style, svg.mermaid g'(el, text) {
|
||||
// We don't want to include the content of these elements in the copied text.
|
||||
return '';
|
||||
},
|
||||
},
|
||||
MathFilter: {
|
||||
'pre.code.math[data-math-style=display]'(el, text) {
|
||||
return `\`\`\`math\n${text.trim()}\n\`\`\``;
|
||||
},
|
||||
'code.code.math[data-math-style=inline]'(el, text) {
|
||||
return `$\`${text}\`$`;
|
||||
},
|
||||
'span.katex-display span.katex-mathml'(el) {
|
||||
const mathAnnotation = el.querySelector('annotation[encoding="application/x-tex"]');
|
||||
if (!mathAnnotation) return false;
|
||||
|
||||
return `\`\`\`math\n${CopyAsGFM.nodeToGFM(mathAnnotation)}\n\`\`\``;
|
||||
},
|
||||
'span.katex-mathml'(el) {
|
||||
const mathAnnotation = el.querySelector('annotation[encoding="application/x-tex"]');
|
||||
if (!mathAnnotation) return false;
|
||||
|
||||
return `$\`${CopyAsGFM.nodeToGFM(mathAnnotation)}\`$`;
|
||||
},
|
||||
'span.katex-html'(el) {
|
||||
// We don't want to include the content of this element in the copied text.
|
||||
return '';
|
||||
},
|
||||
'annotation[encoding="application/x-tex"]'(el, text) {
|
||||
return text.trim();
|
||||
},
|
||||
},
|
||||
SanitizationFilter: {
|
||||
'a[name]:not([href]):empty'(el) {
|
||||
return el.outerHTML;
|
||||
},
|
||||
dl(el, text) {
|
||||
let lines = text
|
||||
.replace(/\n\n/g, '\n')
|
||||
.trim()
|
||||
.split('\n');
|
||||
// Add two spaces to the front of subsequent list items lines,
|
||||
// or leave the line entirely blank.
|
||||
lines = lines.map(l => {
|
||||
const line = l.trim();
|
||||
if (line.length === 0) return '';
|
||||
|
||||
return ` ${line}`;
|
||||
});
|
||||
|
||||
return `<dl>\n${lines.join('\n')}\n</dl>\n`;
|
||||
},
|
||||
'dt, dd, summary, details'(el, text) {
|
||||
const tag = el.nodeName.toLowerCase();
|
||||
return `<${tag}>${text}</${tag}>\n`;
|
||||
},
|
||||
'sup, sub, kbd, q, samp, var, ruby, rt, rp, abbr'(el, text) {
|
||||
const tag = el.nodeName.toLowerCase();
|
||||
return `<${tag}>${text}</${tag}>`;
|
||||
},
|
||||
},
|
||||
SyntaxHighlightFilter: {
|
||||
'pre.code.highlight'(el, t) {
|
||||
const text = t.trimRight();
|
||||
|
||||
let lang = el.getAttribute('lang');
|
||||
if (!lang || lang === 'plaintext') {
|
||||
lang = '';
|
||||
}
|
||||
|
||||
// Prefixes lines with 4 spaces if the code contains triple backticks
|
||||
if (lang === '' && text.match(/^```/gm)) {
|
||||
return text
|
||||
.split('\n')
|
||||
.map(l => {
|
||||
const line = l.trim();
|
||||
if (line.length === 0) return '';
|
||||
|
||||
return ` ${line}`;
|
||||
})
|
||||
.join('\n');
|
||||
}
|
||||
|
||||
return `\`\`\`${lang}\n${text}\n\`\`\``;
|
||||
},
|
||||
'pre > code'(el, text) {
|
||||
// Don't wrap code blocks in ``
|
||||
return text;
|
||||
},
|
||||
},
|
||||
MarkdownFilter: {
|
||||
br(el) {
|
||||
// Two spaces at the end of a line are turned into a BR
|
||||
return ' ';
|
||||
},
|
||||
code(el, text) {
|
||||
let backtickCount = 1;
|
||||
const backtickMatch = text.match(/`+/);
|
||||
if (backtickMatch) {
|
||||
backtickCount = backtickMatch[0].length + 1;
|
||||
}
|
||||
|
||||
const backticks = Array(backtickCount + 1).join('`');
|
||||
const spaceOrNoSpace = backtickCount > 1 ? ' ' : '';
|
||||
|
||||
return backticks + spaceOrNoSpace + text.trim() + spaceOrNoSpace + backticks;
|
||||
},
|
||||
blockquote(el, text) {
|
||||
return text
|
||||
.trim()
|
||||
.split('\n')
|
||||
.map(s => `> ${s}`.trim())
|
||||
.join('\n');
|
||||
},
|
||||
img(el) {
|
||||
const imageSrc = el.src;
|
||||
const imageUrl = imageSrc && imageSrc !== placeholderImage ? imageSrc : el.dataset.src || '';
|
||||
return `![${el.getAttribute('alt')}](${imageUrl})`;
|
||||
},
|
||||
'a.anchor'(el, text) {
|
||||
// Don't render a Markdown link for the anchor link inside a heading
|
||||
return text;
|
||||
},
|
||||
a(el, text) {
|
||||
return `[${text}](${el.getAttribute('href')})`;
|
||||
},
|
||||
li(el, text) {
|
||||
const lines = text.trim().split('\n');
|
||||
const firstLine = `- ${lines.shift()}`;
|
||||
// Add four spaces to the front of subsequent list items lines,
|
||||
// or leave the line entirely blank.
|
||||
const nextLines = lines.map(s => {
|
||||
if (s.trim().length === 0) return '';
|
||||
|
||||
return ` ${s}`;
|
||||
});
|
||||
|
||||
return `${firstLine}\n${nextLines.join('\n')}`;
|
||||
},
|
||||
ul(el, text) {
|
||||
return text;
|
||||
},
|
||||
ol(el, text) {
|
||||
// LIs get a `- ` prefix by default, which we replace by `1. ` for ordered lists.
|
||||
return text.replace(/^- /gm, '1. ');
|
||||
},
|
||||
h1(el, text) {
|
||||
return `# ${text.trim()}\n`;
|
||||
},
|
||||
h2(el, text) {
|
||||
return `## ${text.trim()}\n`;
|
||||
},
|
||||
h3(el, text) {
|
||||
return `### ${text.trim()}\n`;
|
||||
},
|
||||
h4(el, text) {
|
||||
return `#### ${text.trim()}\n`;
|
||||
},
|
||||
h5(el, text) {
|
||||
return `##### ${text.trim()}\n`;
|
||||
},
|
||||
h6(el, text) {
|
||||
return `###### ${text.trim()}\n`;
|
||||
},
|
||||
strong(el, text) {
|
||||
return `**${text}**`;
|
||||
},
|
||||
em(el, text) {
|
||||
return `_${text}_`;
|
||||
},
|
||||
del(el, text) {
|
||||
return `~~${text}~~`;
|
||||
},
|
||||
hr(el) {
|
||||
// extra leading \n is to ensure that there is a blank line between
|
||||
// a list followed by an hr, otherwise this breaks old redcarpet rendering
|
||||
return '\n-----\n';
|
||||
},
|
||||
p(el, text) {
|
||||
return `${text.trim()}\n`;
|
||||
},
|
||||
table(el) {
|
||||
const theadEl = el.querySelector('thead');
|
||||
const tbodyEl = el.querySelector('tbody');
|
||||
if (!theadEl || !tbodyEl) return false;
|
||||
|
||||
const theadText = CopyAsGFM.nodeToGFM(theadEl);
|
||||
const tbodyText = CopyAsGFM.nodeToGFM(tbodyEl);
|
||||
|
||||
return [theadText, tbodyText].join('\n');
|
||||
},
|
||||
thead(el, text) {
|
||||
const cells = _.map(el.querySelectorAll('th'), cell => {
|
||||
let chars = CopyAsGFM.nodeToGFM(cell).length + 2;
|
||||
|
||||
let before = '';
|
||||
let after = '';
|
||||
const alignment = cell.align || cell.style.textAlign;
|
||||
|
||||
switch (alignment) {
|
||||
case 'center':
|
||||
before = ':';
|
||||
after = ':';
|
||||
chars -= 2;
|
||||
break;
|
||||
case 'right':
|
||||
after = ':';
|
||||
chars -= 1;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
chars = Math.max(chars, 3);
|
||||
|
||||
const middle = Array(chars + 1).join('-');
|
||||
|
||||
return before + middle + after;
|
||||
});
|
||||
|
||||
const separatorRow = `|${cells.join('|')}|`;
|
||||
|
||||
return [text, separatorRow].join('\n');
|
||||
},
|
||||
tr(el) {
|
||||
const cellEls = el.querySelectorAll('td, th');
|
||||
if (cellEls.length === 0) return false;
|
||||
|
||||
const cells = _.map(cellEls, cell => CopyAsGFM.nodeToGFM(cell));
|
||||
return `| ${cells.join(' | ')} |`;
|
||||
},
|
||||
},
|
||||
};
|
||||
import { DOMParser } from 'prosemirror-model';
|
||||
import { getSelectedFragment } from '~/lib/utils/common_utils';
|
||||
import schema from './schema';
|
||||
import markdownSerializer from './serializer';
|
||||
|
||||
export class CopyAsGFM {
|
||||
constructor() {
|
||||
|
@ -347,8 +35,13 @@ export class CopyAsGFM {
|
|||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
const div = document.createElement('div');
|
||||
div.appendChild(el.cloneNode(true));
|
||||
const html = div.innerHTML;
|
||||
|
||||
clipboardData.setData('text/plain', el.textContent);
|
||||
clipboardData.setData('text/x-gfm', this.nodeToGFM(el));
|
||||
clipboardData.setData('text/html', html);
|
||||
}
|
||||
|
||||
static pasteGFM(e) {
|
||||
|
@ -361,7 +54,7 @@ export class CopyAsGFM {
|
|||
|
||||
e.preventDefault();
|
||||
|
||||
window.gl.utils.insertText(e.target, (textBefore, textAfter) => {
|
||||
window.gl.utils.insertText(e.target, textBefore => {
|
||||
// If the text before the cursor contains an odd number of backticks,
|
||||
// we are either inside an inline code span that starts with 1 backtick
|
||||
// or a code block that starts with 3 backticks.
|
||||
|
@ -443,75 +136,12 @@ export class CopyAsGFM {
|
|||
return codeElement;
|
||||
}
|
||||
|
||||
static nodeToGFM(node, respectWhitespaceParam = false) {
|
||||
if (node.nodeType === Node.COMMENT_NODE) {
|
||||
return '';
|
||||
}
|
||||
static nodeToGFM(node) {
|
||||
const wrapEl = document.createElement('div');
|
||||
wrapEl.appendChild(node.cloneNode(true));
|
||||
const doc = DOMParser.fromSchema(schema).parse(wrapEl);
|
||||
|
||||
if (node.nodeType === Node.TEXT_NODE) {
|
||||
return node.textContent;
|
||||
}
|
||||
|
||||
const respectWhitespace =
|
||||
respectWhitespaceParam || (node.nodeName === 'PRE' || node.nodeName === 'CODE');
|
||||
|
||||
const text = this.innerGFM(node, respectWhitespace);
|
||||
|
||||
if (node.nodeType === Node.DOCUMENT_FRAGMENT_NODE) {
|
||||
return text;
|
||||
}
|
||||
|
||||
for (const filter in gfmRules) {
|
||||
const rules = gfmRules[filter];
|
||||
|
||||
for (const selector in rules) {
|
||||
const func = rules[selector];
|
||||
|
||||
if (!nodeMatchesSelector(node, selector)) continue;
|
||||
|
||||
let result;
|
||||
if (func.length === 2) {
|
||||
// if `func` takes 2 arguments, it depends on text.
|
||||
// if there is no text, we don't need to generate GFM for this node.
|
||||
if (text.length === 0) continue;
|
||||
|
||||
result = func(node, text);
|
||||
} else {
|
||||
result = func(node);
|
||||
}
|
||||
|
||||
if (result === false) continue;
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
return text;
|
||||
}
|
||||
|
||||
static innerGFM(parentNode, respectWhitespace = false) {
|
||||
const nodes = parentNode.childNodes;
|
||||
|
||||
const clonedParentNode = parentNode.cloneNode(true);
|
||||
const clonedNodes = Array.prototype.slice.call(clonedParentNode.childNodes, 0);
|
||||
|
||||
for (let i = 0; i < nodes.length; i += 1) {
|
||||
const node = nodes[i];
|
||||
const clonedNode = clonedNodes[i];
|
||||
|
||||
const text = this.nodeToGFM(node, respectWhitespace);
|
||||
|
||||
// `clonedNode.replaceWith(text)` is not yet widely supported
|
||||
clonedNode.parentNode.replaceChild(document.createTextNode(text), clonedNode);
|
||||
}
|
||||
|
||||
let nodeText = clonedParentNode.innerText || clonedParentNode.textContent;
|
||||
|
||||
if (!respectWhitespace) {
|
||||
nodeText = nodeText.trim();
|
||||
}
|
||||
|
||||
return nodeText;
|
||||
return markdownSerializer.serialize(doc);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,106 @@
|
|||
import Doc from './nodes/doc';
|
||||
import Paragraph from './nodes/paragraph';
|
||||
import Text from './nodes/text';
|
||||
|
||||
import Blockquote from './nodes/blockquote';
|
||||
import CodeBlock from './nodes/code_block';
|
||||
import HardBreak from './nodes/hard_break';
|
||||
import Heading from './nodes/heading';
|
||||
import HorizontalRule from './nodes/horizontal_rule';
|
||||
import Image from './nodes/image';
|
||||
|
||||
import Table from './nodes/table';
|
||||
import TableHead from './nodes/table_head';
|
||||
import TableBody from './nodes/table_body';
|
||||
import TableHeaderRow from './nodes/table_header_row';
|
||||
import TableRow from './nodes/table_row';
|
||||
import TableCell from './nodes/table_cell';
|
||||
|
||||
import Emoji from './nodes/emoji';
|
||||
import Reference from './nodes/reference';
|
||||
|
||||
import TableOfContents from './nodes/table_of_contents';
|
||||
import Video from './nodes/video';
|
||||
|
||||
import BulletList from './nodes/bullet_list';
|
||||
import OrderedList from './nodes/ordered_list';
|
||||
import ListItem from './nodes/list_item';
|
||||
|
||||
import DescriptionList from './nodes/description_list';
|
||||
import DescriptionTerm from './nodes/description_term';
|
||||
import DescriptionDetails from './nodes/description_details';
|
||||
|
||||
import TaskList from './nodes/task_list';
|
||||
import OrderedTaskList from './nodes/ordered_task_list';
|
||||
import TaskListItem from './nodes/task_list_item';
|
||||
|
||||
import Summary from './nodes/summary';
|
||||
import Details from './nodes/details';
|
||||
|
||||
import Bold from './marks/bold';
|
||||
import Italic from './marks/italic';
|
||||
import Strike from './marks/strike';
|
||||
import InlineDiff from './marks/inline_diff';
|
||||
|
||||
import Link from './marks/link';
|
||||
import Code from './marks/code';
|
||||
import MathMark from './marks/math';
|
||||
import InlineHTML from './marks/inline_html';
|
||||
|
||||
// The filters referenced in lib/banzai/pipeline/gfm_pipeline.rb transform
|
||||
// GitLab Flavored Markdown (GFM) to HTML.
|
||||
// The nodes and marks referenced here transform that same HTML to GFM to be copied to the clipboard.
|
||||
// Every filter in lib/banzai/pipeline/gfm_pipeline.rb that generates HTML
|
||||
// from GFM should have a node or mark here.
|
||||
// The GFM-to-HTML-to-GFM cycle is tested in spec/features/copy_as_gfm_spec.rb.
|
||||
|
||||
export default [
|
||||
new Doc(),
|
||||
new Paragraph(),
|
||||
new Text(),
|
||||
|
||||
new Blockquote(),
|
||||
new CodeBlock(),
|
||||
new HardBreak(),
|
||||
new Heading({ maxLevel: 6 }),
|
||||
new HorizontalRule(),
|
||||
new Image(),
|
||||
|
||||
new Table(),
|
||||
new TableHead(),
|
||||
new TableBody(),
|
||||
new TableHeaderRow(),
|
||||
new TableRow(),
|
||||
new TableCell(),
|
||||
|
||||
new Emoji(),
|
||||
new Reference(),
|
||||
|
||||
new TableOfContents(),
|
||||
new Video(),
|
||||
|
||||
new BulletList(),
|
||||
new OrderedList(),
|
||||
new ListItem(),
|
||||
|
||||
new DescriptionList(),
|
||||
new DescriptionTerm(),
|
||||
new DescriptionDetails(),
|
||||
|
||||
new TaskList(),
|
||||
new OrderedTaskList(),
|
||||
new TaskListItem(),
|
||||
|
||||
new Summary(),
|
||||
new Details(),
|
||||
|
||||
new Bold(),
|
||||
new Italic(),
|
||||
new Strike(),
|
||||
new InlineDiff(),
|
||||
|
||||
new Link(),
|
||||
new Code(),
|
||||
new MathMark(),
|
||||
new InlineHTML(),
|
||||
];
|
|
@ -0,0 +1,11 @@
|
|||
/* eslint-disable class-methods-use-this */
|
||||
|
||||
import { Bold as BaseBold } from 'tiptap-extensions';
|
||||
import { defaultMarkdownSerializer } from 'prosemirror-markdown';
|
||||
|
||||
// Transforms generated HTML back to GFM for Banzai::Filter::MarkdownFilter
|
||||
export default class Bold extends BaseBold {
|
||||
get toMarkdown() {
|
||||
return defaultMarkdownSerializer.marks.strong;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
/* eslint-disable class-methods-use-this */
|
||||
|
||||
import { Code as BaseCode } from 'tiptap-extensions';
|
||||
import { defaultMarkdownSerializer } from 'prosemirror-markdown';
|
||||
|
||||
// Transforms generated HTML back to GFM for Banzai::Filter::MarkdownFilter
|
||||
export default class Code extends BaseCode {
|
||||
get toMarkdown() {
|
||||
return defaultMarkdownSerializer.marks.code;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
/* eslint-disable class-methods-use-this */
|
||||
|
||||
import { Mark } from 'tiptap';
|
||||
|
||||
// Transforms generated HTML back to GFM for Banzai::Filter::InlineDiffFilter
|
||||
export default class InlineDiff extends Mark {
|
||||
get name() {
|
||||
return 'inline_diff';
|
||||
}
|
||||
|
||||
get schema() {
|
||||
return {
|
||||
attrs: {
|
||||
addition: {
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
parseDOM: [
|
||||
{ tag: 'span.idiff.addition', attrs: { addition: true } },
|
||||
{ tag: 'span.idiff.deletion', attrs: { addition: false } },
|
||||
],
|
||||
toDOM: node => [
|
||||
'span',
|
||||
{ class: `idiff left right ${node.attrs.addition ? 'addition' : 'deletion'}` },
|
||||
0,
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
get toMarkdown() {
|
||||
return {
|
||||
mixable: true,
|
||||
open(state, mark) {
|
||||
return mark.attrs.addition ? '{+' : '{-';
|
||||
},
|
||||
close(state, mark) {
|
||||
return mark.attrs.addition ? '+}' : '-}';
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
/* eslint-disable class-methods-use-this */
|
||||
|
||||
import { Mark } from 'tiptap';
|
||||
import _ from 'underscore';
|
||||
|
||||
// Transforms generated HTML back to GFM for Banzai::Filter::MarkdownFilter
|
||||
export default class InlineHTML extends Mark {
|
||||
get name() {
|
||||
return 'inline_html';
|
||||
}
|
||||
|
||||
get schema() {
|
||||
return {
|
||||
excludes: '',
|
||||
attrs: {
|
||||
tag: {},
|
||||
title: { default: null },
|
||||
},
|
||||
parseDOM: [
|
||||
{
|
||||
tag: 'sup, sub, kbd, q, samp, var',
|
||||
getAttrs: el => ({ tag: el.nodeName.toLowerCase() }),
|
||||
},
|
||||
{
|
||||
tag: 'abbr',
|
||||
getAttrs: el => ({ tag: 'abbr', title: el.getAttribute('title') }),
|
||||
},
|
||||
],
|
||||
toDOM: node => [node.attrs.tag, { title: node.attrs.title }, 0],
|
||||
};
|
||||
}
|
||||
|
||||
get toMarkdown() {
|
||||
return {
|
||||
mixable: true,
|
||||
open(state, mark) {
|
||||
return `<${mark.attrs.tag}${
|
||||
mark.attrs.title ? ` title="${state.esc(_.escape(mark.attrs.title))}"` : ''
|
||||
}>`;
|
||||
},
|
||||
close(state, mark) {
|
||||
return `</${mark.attrs.tag}>`;
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
/* eslint-disable class-methods-use-this */
|
||||
|
||||
import { Italic as BaseItalic } from 'tiptap-extensions';
|
||||
import { defaultMarkdownSerializer } from 'prosemirror-markdown';
|
||||
|
||||
// Transforms generated HTML back to GFM for Banzai::Filter::MarkdownFilter
|
||||
export default class Italic extends BaseItalic {
|
||||
get toMarkdown() {
|
||||
return defaultMarkdownSerializer.marks.em;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
/* eslint-disable class-methods-use-this */
|
||||
|
||||
import { Link as BaseLink } from 'tiptap-extensions';
|
||||
import { defaultMarkdownSerializer } from 'prosemirror-markdown';
|
||||
|
||||
// Transforms generated HTML back to GFM for Banzai::Filter::MarkdownFilter
|
||||
export default class Link extends BaseLink {
|
||||
get toMarkdown() {
|
||||
return {
|
||||
mixable: true,
|
||||
open(state, mark, parent, index) {
|
||||
const open = defaultMarkdownSerializer.marks.link.open(state, mark, parent, index);
|
||||
return open === '<' ? '' : open;
|
||||
},
|
||||
close(state, mark, parent, index) {
|
||||
const close = defaultMarkdownSerializer.marks.link.close(state, mark, parent, index);
|
||||
return close === '>' ? '' : close;
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
/* eslint-disable class-methods-use-this */
|
||||
|
||||
import { Mark } from 'tiptap';
|
||||
import { defaultMarkdownSerializer } from 'prosemirror-markdown';
|
||||
|
||||
// Transforms generated HTML back to GFM for Banzai::Filter::MathFilter
|
||||
export default class MathMark extends Mark {
|
||||
get name() {
|
||||
return 'math';
|
||||
}
|
||||
|
||||
get schema() {
|
||||
return {
|
||||
parseDOM: [
|
||||
// Matches HTML generated by Banzai::Filter::MathFilter
|
||||
{
|
||||
tag: 'code.code.math[data-math-style=inline]',
|
||||
priority: 51,
|
||||
},
|
||||
// Matches HTML after being transformed by app/assets/javascripts/behaviors/markdown/render_math.js
|
||||
{
|
||||
tag: 'span.katex',
|
||||
contentElement: 'annotation[encoding="application/x-tex"]',
|
||||
},
|
||||
],
|
||||
toDOM: () => ['code', { class: 'code math', 'data-math-style': 'inline' }, 0],
|
||||
};
|
||||
}
|
||||
|
||||
get toMarkdown() {
|
||||
return {
|
||||
escape: false,
|
||||
open(state, mark, parent, index) {
|
||||
return `$${defaultMarkdownSerializer.marks.code.open(state, mark, parent, index)}`;
|
||||
},
|
||||
close(state, mark, parent, index) {
|
||||
return `${defaultMarkdownSerializer.marks.code.close(state, mark, parent, index)}$`;
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
/* eslint-disable class-methods-use-this */
|
||||
|
||||
import { Strike as BaseStrike } from 'tiptap-extensions';
|
||||
|
||||
// Transforms generated HTML back to GFM for Banzai::Filter::MarkdownFilter
|
||||
export default class Strike extends BaseStrike {
|
||||
get toMarkdown() {
|
||||
return {
|
||||
open: '~~',
|
||||
close: '~~',
|
||||
mixable: true,
|
||||
expelEnclosingWhitespace: true,
|
||||
};
|
||||
}
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
/* eslint-disable class-methods-use-this */
|
||||
|
||||
import { Blockquote as BaseBlockquote } from 'tiptap-extensions';
|
||||
import { defaultMarkdownSerializer } from 'prosemirror-markdown';
|
||||
|
||||
// Transforms generated HTML back to GFM for Banzai::Filter::MarkdownFilter
|
||||
export default class Blockquote extends BaseBlockquote {
|
||||
toMarkdown(state, node) {
|
||||
if (!node.childCount) return;
|
||||
|
||||
defaultMarkdownSerializer.nodes.blockquote(state, node);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
/* eslint-disable class-methods-use-this */
|
||||
|
||||
import { BulletList as BaseBulletList } from 'tiptap-extensions';
|
||||
import { defaultMarkdownSerializer } from 'prosemirror-markdown';
|
||||
|
||||
// Transforms generated HTML back to GFM for Banzai::Filter::MarkdownFilter
|
||||
export default class BulletList extends BaseBulletList {
|
||||
toMarkdown(state, node) {
|
||||
defaultMarkdownSerializer.nodes.bullet_list(state, node);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,79 @@
|
|||
/* eslint-disable class-methods-use-this */
|
||||
|
||||
import { CodeBlock as BaseCodeBlock } from 'tiptap-extensions';
|
||||
|
||||
const PLAINTEXT_LANG = 'plaintext';
|
||||
|
||||
// Transforms generated HTML back to GFM for:
|
||||
// - Banzai::Filter::SyntaxHighlightFilter
|
||||
// - Banzai::Filter::MathFilter
|
||||
// - Banzai::Filter::MermaidFilter
|
||||
export default class CodeBlock extends BaseCodeBlock {
|
||||
get schema() {
|
||||
return {
|
||||
content: 'text*',
|
||||
marks: '',
|
||||
group: 'block',
|
||||
code: true,
|
||||
defining: true,
|
||||
attrs: {
|
||||
lang: { default: PLAINTEXT_LANG },
|
||||
},
|
||||
parseDOM: [
|
||||
// Matches HTML generated by Banzai::Filter::SyntaxHighlightFilter, Banzai::Filter::MathFilter or Banzai::Filter::MermaidFilter
|
||||
{
|
||||
tag: 'pre.code.highlight',
|
||||
preserveWhitespace: 'full',
|
||||
getAttrs: el => {
|
||||
const lang = el.getAttribute('lang');
|
||||
if (!lang || lang === '') return {};
|
||||
|
||||
return { lang };
|
||||
},
|
||||
},
|
||||
// Matches HTML generated by Banzai::Filter::MathFilter,
|
||||
// after being transformed by app/assets/javascripts/behaviors/markdown/render_math.js
|
||||
{
|
||||
tag: 'span.katex-display',
|
||||
preserveWhitespace: 'full',
|
||||
contentElement: 'annotation[encoding="application/x-tex"]',
|
||||
attrs: { lang: 'math' },
|
||||
},
|
||||
// Matches HTML generated by Banzai::Filter::MathFilter,
|
||||
// after being transformed by app/assets/javascripts/behaviors/markdown/render_mermaid.js
|
||||
{
|
||||
tag: 'svg.mermaid',
|
||||
preserveWhitespace: 'full',
|
||||
contentElement: 'text.source',
|
||||
attrs: { lang: 'mermaid' },
|
||||
},
|
||||
],
|
||||
toDOM: node => ['pre', { class: 'code highlight', lang: node.attrs.lang }, ['code', 0]],
|
||||
};
|
||||
}
|
||||
|
||||
toMarkdown(state, node) {
|
||||
if (!node.childCount) return;
|
||||
|
||||
const {
|
||||
textContent: text,
|
||||
attrs: { lang },
|
||||
} = node;
|
||||
|
||||
// Prefixes lines with 4 spaces if the code contains a line that starts with triple backticks
|
||||
if (lang === PLAINTEXT_LANG && text.match(/^```/gm)) {
|
||||
state.wrapBlock(' ', null, node, () => state.text(text, false));
|
||||
return;
|
||||
}
|
||||
|
||||
state.write('```');
|
||||
if (lang !== PLAINTEXT_LANG) state.write(lang);
|
||||
|
||||
state.ensureNewLine();
|
||||
state.text(text, false);
|
||||
state.ensureNewLine();
|
||||
|
||||
state.write('```');
|
||||
state.closeBlock(node);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
/* eslint-disable class-methods-use-this */
|
||||
|
||||
import { Node } from 'tiptap';
|
||||
|
||||
// Transforms generated HTML back to GFM for Banzai::Filter::MarkdownFilter
|
||||
export default class DescriptionDetails extends Node {
|
||||
get name() {
|
||||
return 'description_details';
|
||||
}
|
||||
|
||||
get schema() {
|
||||
return {
|
||||
content: 'text*',
|
||||
marks: '',
|
||||
defining: true,
|
||||
parseDOM: [{ tag: 'dd' }],
|
||||
toDOM: () => ['dd', 0],
|
||||
};
|
||||
}
|
||||
|
||||
toMarkdown(state, node) {
|
||||
state.flushClose(1);
|
||||
state.write('<dd>');
|
||||
state.text(node.textContent, false);
|
||||
state.write('</dd>');
|
||||
state.closeBlock(node);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
/* eslint-disable class-methods-use-this */
|
||||
|
||||
import { Node } from 'tiptap';
|
||||
|
||||
// Transforms generated HTML back to GFM for Banzai::Filter::MarkdownFilter
|
||||
export default class DescriptionList extends Node {
|
||||
get name() {
|
||||
return 'description_list';
|
||||
}
|
||||
|
||||
get schema() {
|
||||
return {
|
||||
content: '(description_term+ description_details+)+',
|
||||
group: 'block',
|
||||
parseDOM: [{ tag: 'dl' }],
|
||||
toDOM: () => ['dl', 0],
|
||||
};
|
||||
}
|
||||
|
||||
toMarkdown(state, node) {
|
||||
state.write('<dl>\n');
|
||||
state.wrapBlock(' ', null, node, () => state.renderContent(node));
|
||||
state.flushClose(1);
|
||||
state.ensureNewLine();
|
||||
state.write('</dl>');
|
||||
state.closeBlock(node);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
/* eslint-disable class-methods-use-this */
|
||||
|
||||
import { Node } from 'tiptap';
|
||||
|
||||
// Transforms generated HTML back to GFM for Banzai::Filter::MarkdownFilter
|
||||
export default class DescriptionTerm extends Node {
|
||||
get name() {
|
||||
return 'description_term';
|
||||
}
|
||||
|
||||
get schema() {
|
||||
return {
|
||||
content: 'text*',
|
||||
marks: '',
|
||||
defining: true,
|
||||
parseDOM: [{ tag: 'dt' }],
|
||||
toDOM: () => ['dt', 0],
|
||||
};
|
||||
}
|
||||
|
||||
toMarkdown(state, node) {
|
||||
state.flushClose(state.closed && state.closed.type === node.type ? 1 : 2);
|
||||
state.write('<dt>');
|
||||
state.text(node.textContent, false);
|
||||
state.write('</dt>');
|
||||
state.closeBlock(node);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
/* eslint-disable class-methods-use-this */
|
||||
|
||||
import { Node } from 'tiptap';
|
||||
|
||||
// Transforms generated HTML back to GFM for Banzai::Filter::MarkdownFilter
|
||||
export default class Details extends Node {
|
||||
get name() {
|
||||
return 'details';
|
||||
}
|
||||
|
||||
get schema() {
|
||||
return {
|
||||
content: 'summary block*',
|
||||
group: 'block',
|
||||
parseDOM: [{ tag: 'details' }],
|
||||
toDOM: () => ['details', { open: true, onclick: 'return false', tabindex: '-1' }, 0],
|
||||
};
|
||||
}
|
||||
|
||||
toMarkdown(state, node) {
|
||||
state.write('<details>\n');
|
||||
state.renderContent(node);
|
||||
state.flushClose(1);
|
||||
state.ensureNewLine();
|
||||
state.write('</details>');
|
||||
state.closeBlock(node);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
/* eslint-disable class-methods-use-this */
|
||||
|
||||
import { Node } from 'tiptap';
|
||||
|
||||
export default class Doc extends Node {
|
||||
get name() {
|
||||
return 'doc';
|
||||
}
|
||||
|
||||
get schema() {
|
||||
return {
|
||||
content: 'block+',
|
||||
};
|
||||
}
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
/* eslint-disable class-methods-use-this */
|
||||
|
||||
import { Node } from 'tiptap';
|
||||
|
||||
// Transforms generated HTML back to GFM for Banzai::Filter::EmojiFilter
|
||||
export default class Emoji extends Node {
|
||||
get name() {
|
||||
return 'emoji';
|
||||
}
|
||||
|
||||
get schema() {
|
||||
return {
|
||||
inline: true,
|
||||
group: 'inline',
|
||||
attrs: {
|
||||
name: {},
|
||||
title: {},
|
||||
moji: {},
|
||||
},
|
||||
parseDOM: [
|
||||
{
|
||||
tag: 'gl-emoji',
|
||||
getAttrs: el => ({
|
||||
name: el.dataset.name,
|
||||
title: el.getAttribute('title'),
|
||||
moji: el.textContent,
|
||||
}),
|
||||
},
|
||||
],
|
||||
toDOM: node => [
|
||||
'gl-emoji',
|
||||
{ 'data-name': node.attrs.name, title: node.attrs.title },
|
||||
node.attrs.moji,
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
toMarkdown(state, node) {
|
||||
state.write(`:${node.attrs.name}:`);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
/* eslint-disable class-methods-use-this */
|
||||
|
||||
import { HardBreak as BaseHardBreak } from 'tiptap-extensions';
|
||||
|
||||
// Transforms generated HTML back to GFM for Banzai::Filter::MarkdownFilter
|
||||
export default class HardBreak extends BaseHardBreak {
|
||||
toMarkdown(state) {
|
||||
if (!state.atBlank()) state.write(' \n');
|
||||
}
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
/* eslint-disable class-methods-use-this */
|
||||
|
||||
import { Heading as BaseHeading } from 'tiptap-extensions';
|
||||
import { defaultMarkdownSerializer } from 'prosemirror-markdown';
|
||||
|
||||
// Transforms generated HTML back to GFM for Banzai::Filter::MarkdownFilter
|
||||
export default class Heading extends BaseHeading {
|
||||
toMarkdown(state, node) {
|
||||
if (!node.childCount) return;
|
||||
|
||||
defaultMarkdownSerializer.nodes.heading(state, node);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
/* eslint-disable class-methods-use-this */
|
||||
|
||||
import { HorizontalRule as BaseHorizontalRule } from 'tiptap-extensions';
|
||||
import { defaultMarkdownSerializer } from 'prosemirror-markdown';
|
||||
|
||||
// Transforms generated HTML back to GFM for Banzai::Filter::MarkdownFilter
|
||||
export default class HorizontalRule extends BaseHorizontalRule {
|
||||
toMarkdown(state, node) {
|
||||
defaultMarkdownSerializer.nodes.horizontal_rule(state, node);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,52 @@
|
|||
/* eslint-disable class-methods-use-this */
|
||||
|
||||
import { Image as BaseImage } from 'tiptap-extensions';
|
||||
import { placeholderImage } from '~/lazy_loader';
|
||||
import { defaultMarkdownSerializer } from 'prosemirror-markdown';
|
||||
|
||||
export default class Image extends BaseImage {
|
||||
get schema() {
|
||||
return {
|
||||
attrs: {
|
||||
src: {},
|
||||
alt: {
|
||||
default: null,
|
||||
},
|
||||
title: {
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
group: 'inline',
|
||||
inline: true,
|
||||
draggable: true,
|
||||
parseDOM: [
|
||||
// Matches HTML generated by Banzai::Filter::ImageLinkFilter
|
||||
{
|
||||
tag: 'a.no-attachment-icon',
|
||||
priority: 51,
|
||||
skip: true,
|
||||
},
|
||||
// Matches HTML generated by Banzai::Filter::ImageLazyLoadFilter
|
||||
{
|
||||
tag: 'img[src]',
|
||||
getAttrs: el => {
|
||||
const imageSrc = el.src;
|
||||
const imageUrl =
|
||||
imageSrc && imageSrc !== placeholderImage ? imageSrc : el.dataset.src || '';
|
||||
|
||||
return {
|
||||
src: imageUrl,
|
||||
title: el.getAttribute('title'),
|
||||
alt: el.getAttribute('alt'),
|
||||
};
|
||||
},
|
||||
},
|
||||
],
|
||||
toDOM: node => ['img', node.attrs],
|
||||
};
|
||||
}
|
||||
|
||||
toMarkdown(state, node) {
|
||||
defaultMarkdownSerializer.nodes.image(state, node);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
/* eslint-disable class-methods-use-this */
|
||||
|
||||
import { ListItem as BaseListItem } from 'tiptap-extensions';
|
||||
import { defaultMarkdownSerializer } from 'prosemirror-markdown';
|
||||
|
||||
// Transforms generated HTML back to GFM for Banzai::Filter::MarkdownFilter
|
||||
export default class ListItem extends BaseListItem {
|
||||
toMarkdown(state, node) {
|
||||
defaultMarkdownSerializer.nodes.list_item(state, node);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
/* eslint-disable class-methods-use-this */
|
||||
|
||||
import { OrderedList as BaseOrderedList } from 'tiptap-extensions';
|
||||
|
||||
// Transforms generated HTML back to GFM for Banzai::Filter::MarkdownFilter
|
||||
export default class OrderedList extends BaseOrderedList {
|
||||
toMarkdown(state, node) {
|
||||
state.renderList(node, ' ', () => '1. ');
|
||||
}
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
/* eslint-disable class-methods-use-this */
|
||||
|
||||
import { Node } from 'tiptap';
|
||||
|
||||
// Transforms generated HTML back to GFM for Banzai::Filter::TaskListFilter
|
||||
export default class OrderedTaskList extends Node {
|
||||
get name() {
|
||||
return 'ordered_task_list';
|
||||
}
|
||||
|
||||
get schema() {
|
||||
return {
|
||||
group: 'block',
|
||||
content: '(task_list_item|list_item)+',
|
||||
parseDOM: [
|
||||
{
|
||||
priority: 51,
|
||||
tag: 'ol.task-list',
|
||||
},
|
||||
],
|
||||
toDOM: () => ['ol', { class: 'task-list' }, 0],
|
||||
};
|
||||
}
|
||||
|
||||
toMarkdown(state, node) {
|
||||
state.renderList(node, ' ', () => '1. ');
|
||||
}
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
/* eslint-disable class-methods-use-this */
|
||||
|
||||
import { Node } from 'tiptap';
|
||||
import { defaultMarkdownSerializer } from 'prosemirror-markdown';
|
||||
|
||||
// Transforms generated HTML back to GFM for Banzai::Filter::MarkdownFilter
|
||||
export default class Paragraph extends Node {
|
||||
get name() {
|
||||
return 'paragraph';
|
||||
}
|
||||
|
||||
get schema() {
|
||||
return {
|
||||
content: 'inline*',
|
||||
group: 'block',
|
||||
parseDOM: [{ tag: 'p' }],
|
||||
toDOM: () => ['p', 0],
|
||||
};
|
||||
}
|
||||
|
||||
toMarkdown(state, node) {
|
||||
defaultMarkdownSerializer.nodes.paragraph(state, node);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,52 @@
|
|||
/* eslint-disable class-methods-use-this */
|
||||
|
||||
import { Node } from 'tiptap';
|
||||
|
||||
// Transforms generated HTML back to GFM for Banzai::Filter::ReferenceFilter and subclasses
|
||||
export default class Reference extends Node {
|
||||
get name() {
|
||||
return 'reference';
|
||||
}
|
||||
|
||||
get schema() {
|
||||
return {
|
||||
inline: true,
|
||||
group: 'inline',
|
||||
atom: true,
|
||||
attrs: {
|
||||
className: {},
|
||||
referenceType: {},
|
||||
originalText: { default: null },
|
||||
href: {},
|
||||
text: {},
|
||||
},
|
||||
parseDOM: [
|
||||
{
|
||||
tag: 'a.gfm:not([data-link=true])',
|
||||
priority: 51,
|
||||
getAttrs: el => ({
|
||||
className: el.className,
|
||||
referenceType: el.dataset.referenceType,
|
||||
originalText: el.dataset.original,
|
||||
href: el.getAttribute('href'),
|
||||
text: el.textContent,
|
||||
}),
|
||||
},
|
||||
],
|
||||
toDOM: node => [
|
||||
'a',
|
||||
{
|
||||
class: node.attrs.className,
|
||||
href: node.attrs.href,
|
||||
'data-reference-type': node.attrs.referenceType,
|
||||
'data-original': node.attrs.originalText,
|
||||
},
|
||||
node.attrs.text,
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
toMarkdown(state, node) {
|
||||
state.write(node.attrs.originalText || node.attrs.text);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
/* eslint-disable class-methods-use-this */
|
||||
|
||||
import { Node } from 'tiptap';
|
||||
|
||||
// Transforms generated HTML back to GFM for Banzai::Filter::MarkdownFilter
|
||||
export default class Summary extends Node {
|
||||
get name() {
|
||||
return 'summary';
|
||||
}
|
||||
|
||||
get schema() {
|
||||
return {
|
||||
content: 'text*',
|
||||
marks: '',
|
||||
defining: true,
|
||||
parseDOM: [{ tag: 'summary' }],
|
||||
toDOM: () => ['summary', 0],
|
||||
};
|
||||
}
|
||||
|
||||
toMarkdown(state, node) {
|
||||
state.write('<summary>');
|
||||
state.text(node.textContent, false);
|
||||
state.write('</summary>');
|
||||
state.closeBlock(node);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
/* eslint-disable class-methods-use-this */
|
||||
|
||||
import { Node } from 'tiptap';
|
||||
|
||||
// Transforms generated HTML back to GFM for Banzai::Filter::MarkdownFilter
|
||||
export default class Table extends Node {
|
||||
get name() {
|
||||
return 'table';
|
||||
}
|
||||
|
||||
get schema() {
|
||||
return {
|
||||
content: 'table_head table_body',
|
||||
group: 'block',
|
||||
isolating: true,
|
||||
parseDOM: [{ tag: 'table' }],
|
||||
toDOM: () => ['table', 0],
|
||||
};
|
||||
}
|
||||
|
||||
toMarkdown(state, node) {
|
||||
state.renderContent(node);
|
||||
state.closeBlock(node);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
/* eslint-disable class-methods-use-this */
|
||||
|
||||
import { Node } from 'tiptap';
|
||||
|
||||
// Transforms generated HTML back to GFM for Banzai::Filter::MarkdownFilter
|
||||
export default class TableBody extends Node {
|
||||
get name() {
|
||||
return 'table_body';
|
||||
}
|
||||
|
||||
get schema() {
|
||||
return {
|
||||
content: 'table_row+',
|
||||
parseDOM: [{ tag: 'tbody' }],
|
||||
toDOM: () => ['tbody', 0],
|
||||
};
|
||||
}
|
||||
|
||||
toMarkdown(state, node) {
|
||||
state.flushClose(1);
|
||||
state.renderContent(node);
|
||||
state.closeBlock(node);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
/* eslint-disable class-methods-use-this */
|
||||
|
||||
import { Node } from 'tiptap';
|
||||
|
||||
// Transforms generated HTML back to GFM for Banzai::Filter::MarkdownFilter
|
||||
export default class TableCell extends Node {
|
||||
get name() {
|
||||
return 'table_cell';
|
||||
}
|
||||
|
||||
get schema() {
|
||||
return {
|
||||
attrs: {
|
||||
header: { default: false },
|
||||
align: { default: null },
|
||||
},
|
||||
content: 'inline*',
|
||||
isolating: true,
|
||||
parseDOM: [
|
||||
{
|
||||
tag: 'td, th',
|
||||
getAttrs: el => ({
|
||||
header: el.tagName === 'TH',
|
||||
align: el.getAttribute('align') || el.style.textAlign,
|
||||
}),
|
||||
},
|
||||
],
|
||||
toDOM: node => [node.attrs.header ? 'th' : 'td', { align: node.attrs.align }, 0],
|
||||
};
|
||||
}
|
||||
|
||||
toMarkdown(state, node) {
|
||||
state.renderInline(node);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
/* eslint-disable class-methods-use-this */
|
||||
|
||||
import { Node } from 'tiptap';
|
||||
|
||||
// Transforms generated HTML back to GFM for Banzai::Filter::MarkdownFilter
|
||||
export default class TableHead extends Node {
|
||||
get name() {
|
||||
return 'table_head';
|
||||
}
|
||||
|
||||
get schema() {
|
||||
return {
|
||||
content: 'table_header_row',
|
||||
parseDOM: [{ tag: 'thead' }],
|
||||
toDOM: () => ['thead', 0],
|
||||
};
|
||||
}
|
||||
|
||||
toMarkdown(state, node) {
|
||||
state.flushClose(1);
|
||||
state.renderContent(node);
|
||||
state.closeBlock(node);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
/* eslint-disable class-methods-use-this */
|
||||
|
||||
import TableRow from './table_row';
|
||||
|
||||
const CENTER_ALIGN = 'center';
|
||||
|
||||
// Transforms generated HTML back to GFM for Banzai::Filter::MarkdownFilter
|
||||
export default class TableHeaderRow extends TableRow {
|
||||
get name() {
|
||||
return 'table_header_row';
|
||||
}
|
||||
|
||||
get schema() {
|
||||
return {
|
||||
content: 'table_cell+',
|
||||
parseDOM: [
|
||||
{
|
||||
tag: 'thead tr',
|
||||
priority: 51,
|
||||
},
|
||||
],
|
||||
toDOM: () => ['tr', 0],
|
||||
};
|
||||
}
|
||||
|
||||
toMarkdown(state, node) {
|
||||
const cellWidths = super.toMarkdown(state, node);
|
||||
|
||||
state.flushClose(1);
|
||||
|
||||
state.write('|');
|
||||
node.forEach((cell, _, i) => {
|
||||
if (i) state.write('|');
|
||||
|
||||
state.write(cell.attrs.align === CENTER_ALIGN ? ':' : '-');
|
||||
state.write(state.repeat('-', cellWidths[i]));
|
||||
state.write(cell.attrs.align === CENTER_ALIGN || cell.attrs.align === 'right' ? ':' : '-');
|
||||
});
|
||||
state.write('|');
|
||||
|
||||
state.closeBlock(node);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
/* eslint-disable class-methods-use-this */
|
||||
|
||||
import { Node } from 'tiptap';
|
||||
|
||||
// Transforms generated HTML back to GFM for Banzai::Filter::TableOfContentsFilter
|
||||
export default class TableOfContents extends Node {
|
||||
get name() {
|
||||
return 'table_of_contents';
|
||||
}
|
||||
|
||||
get schema() {
|
||||
return {
|
||||
group: 'block',
|
||||
atom: true,
|
||||
parseDOM: [
|
||||
{
|
||||
tag: 'ul.section-nav',
|
||||
priority: 51,
|
||||
},
|
||||
{
|
||||
tag: 'p.table-of-contents',
|
||||
priority: 51,
|
||||
},
|
||||
],
|
||||
toDOM: () => ['p', { class: 'table-of-contents' }, 'Table of Contents'],
|
||||
};
|
||||
}
|
||||
|
||||
toMarkdown(state, node) {
|
||||
state.write('[[_TOC_]]');
|
||||
state.closeBlock(node);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
/* eslint-disable class-methods-use-this */
|
||||
|
||||
import { Node } from 'tiptap';
|
||||
|
||||
// Transforms generated HTML back to GFM for Banzai::Filter::MarkdownFilter
|
||||
export default class TableRow extends Node {
|
||||
get name() {
|
||||
return 'table_row';
|
||||
}
|
||||
|
||||
get schema() {
|
||||
return {
|
||||
content: 'table_cell+',
|
||||
parseDOM: [{ tag: 'tr' }],
|
||||
toDOM: () => ['tr', 0],
|
||||
};
|
||||
}
|
||||
|
||||
toMarkdown(state, node) {
|
||||
const cellWidths = [];
|
||||
|
||||
state.flushClose(1);
|
||||
|
||||
state.write('| ');
|
||||
node.forEach((cell, _, i) => {
|
||||
if (i) state.write(' | ');
|
||||
|
||||
const { length } = state.out;
|
||||
state.render(cell, node, i);
|
||||
cellWidths.push(state.out.length - length);
|
||||
});
|
||||
state.write(' |');
|
||||
|
||||
state.closeBlock(node);
|
||||
|
||||
return cellWidths;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
/* eslint-disable class-methods-use-this */
|
||||
|
||||
import { Node } from 'tiptap';
|
||||
|
||||
// Transforms generated HTML back to GFM for Banzai::Filter::TaskListFilter
|
||||
export default class TaskList extends Node {
|
||||
get name() {
|
||||
return 'task_list';
|
||||
}
|
||||
|
||||
get schema() {
|
||||
return {
|
||||
group: 'block',
|
||||
content: '(task_list_item|list_item)+',
|
||||
parseDOM: [
|
||||
{
|
||||
priority: 51,
|
||||
tag: 'ul.task-list',
|
||||
},
|
||||
],
|
||||
toDOM: () => ['ul', { class: 'task-list' }, 0],
|
||||
};
|
||||
}
|
||||
|
||||
toMarkdown(state, node) {
|
||||
state.renderList(node, ' ', () => '* ');
|
||||
}
|
||||
}
|
|
@ -0,0 +1,49 @@
|
|||
/* eslint-disable class-methods-use-this */
|
||||
|
||||
import { Node } from 'tiptap';
|
||||
|
||||
// Transforms generated HTML back to GFM for Banzai::Filter::TaskListFilter
|
||||
export default class TaskListItem extends Node {
|
||||
get name() {
|
||||
return 'task_list_item';
|
||||
}
|
||||
|
||||
get schema() {
|
||||
return {
|
||||
attrs: {
|
||||
done: {
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
defining: true,
|
||||
draggable: false,
|
||||
content: 'paragraph block*',
|
||||
parseDOM: [
|
||||
{
|
||||
priority: 51,
|
||||
tag: 'li.task-list-item',
|
||||
getAttrs: el => {
|
||||
const checkbox = el.querySelector('input[type=checkbox].task-list-item-checkbox');
|
||||
return { done: checkbox && checkbox.checked };
|
||||
},
|
||||
},
|
||||
],
|
||||
toDOM(node) {
|
||||
return [
|
||||
'li',
|
||||
{ class: 'task-list-item' },
|
||||
[
|
||||
'input',
|
||||
{ type: 'checkbox', class: 'task-list-item-checkbox', checked: node.attrs.done },
|
||||
],
|
||||
['div', { class: 'todo-content' }, 0],
|
||||
];
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
toMarkdown(state, node) {
|
||||
state.write(`[${node.attrs.done ? 'x' : ' '}] `);
|
||||
state.renderContent(node);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
/* eslint-disable class-methods-use-this */
|
||||
|
||||
import { Node } from 'tiptap';
|
||||
import { defaultMarkdownSerializer } from 'prosemirror-markdown';
|
||||
|
||||
export default class Text extends Node {
|
||||
get name() {
|
||||
return 'text';
|
||||
}
|
||||
|
||||
get schema() {
|
||||
return {
|
||||
group: 'inline',
|
||||
};
|
||||
}
|
||||
|
||||
toMarkdown(state, node) {
|
||||
defaultMarkdownSerializer.nodes.text(state, node);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,54 @@
|
|||
/* eslint-disable class-methods-use-this */
|
||||
|
||||
import { Node } from 'tiptap';
|
||||
import { defaultMarkdownSerializer } from 'prosemirror-markdown';
|
||||
|
||||
// Transforms generated HTML back to GFM for Banzai::Filter::VideoLinkFilter
|
||||
export default class Video extends Node {
|
||||
get name() {
|
||||
return 'video';
|
||||
}
|
||||
|
||||
get schema() {
|
||||
return {
|
||||
attrs: {
|
||||
src: {},
|
||||
alt: {
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
group: 'block',
|
||||
draggable: true,
|
||||
parseDOM: [
|
||||
{
|
||||
tag: '.video-container',
|
||||
skip: true,
|
||||
},
|
||||
{
|
||||
tag: '.video-container p',
|
||||
priority: 51,
|
||||
ignore: true,
|
||||
},
|
||||
{
|
||||
tag: 'video[src]',
|
||||
getAttrs: el => ({ src: el.getAttribute('src'), alt: el.dataset.title }),
|
||||
},
|
||||
],
|
||||
toDOM: node => [
|
||||
'video',
|
||||
{
|
||||
src: node.attrs.src,
|
||||
width: '400',
|
||||
controls: true,
|
||||
'data-setup': '{}',
|
||||
'data-title': node.attrs.alt,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
toMarkdown(state, node) {
|
||||
defaultMarkdownSerializer.nodes.image(state, node);
|
||||
state.closeBlock(node);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
import { Schema } from 'prosemirror-model';
|
||||
import editorExtensions from './editor_extensions';
|
||||
|
||||
const nodes = editorExtensions
|
||||
.filter(extension => extension.type === 'node')
|
||||
.reduce(
|
||||
(ns, { name, schema }) => ({
|
||||
...ns,
|
||||
[name]: schema,
|
||||
}),
|
||||
{},
|
||||
);
|
||||
|
||||
const marks = editorExtensions
|
||||
.filter(extension => extension.type === 'mark')
|
||||
.reduce(
|
||||
(ms, { name, schema }) => ({
|
||||
...ms,
|
||||
[name]: schema,
|
||||
}),
|
||||
{},
|
||||
);
|
||||
|
||||
export default new Schema({ nodes, marks });
|
|
@ -0,0 +1,24 @@
|
|||
import { MarkdownSerializer } from 'prosemirror-markdown';
|
||||
import editorExtensions from './editor_extensions';
|
||||
|
||||
const nodes = editorExtensions
|
||||
.filter(extension => extension.type === 'node')
|
||||
.reduce(
|
||||
(ns, { name, toMarkdown }) => ({
|
||||
...ns,
|
||||
[name]: toMarkdown,
|
||||
}),
|
||||
{},
|
||||
);
|
||||
|
||||
const marks = editorExtensions
|
||||
.filter(extension => extension.type === 'mark')
|
||||
.reduce(
|
||||
(ms, { name, toMarkdown }) => ({
|
||||
...ms,
|
||||
[name]: toMarkdown,
|
||||
}),
|
||||
{},
|
||||
);
|
||||
|
||||
export default new MarkdownSerializer(nodes, marks);
|
|
@ -1,6 +1,5 @@
|
|||
import $ from 'jquery';
|
||||
import Mousetrap from 'mousetrap';
|
||||
import _ from 'underscore';
|
||||
import Sidebar from '../../right_sidebar';
|
||||
import Shortcuts from './shortcuts';
|
||||
import { CopyAsGFM } from '../markdown/copy_as_gfm';
|
||||
|
@ -63,18 +62,18 @@ export default class ShortcutsIssuable extends Shortcuts {
|
|||
}
|
||||
|
||||
const el = CopyAsGFM.transformGFMSelection(documentFragment.cloneNode(true));
|
||||
const selected = CopyAsGFM.nodeToGFM(el);
|
||||
const blockquoteEl = document.createElement('blockquote');
|
||||
blockquoteEl.appendChild(el);
|
||||
const text = CopyAsGFM.nodeToGFM(blockquoteEl);
|
||||
|
||||
if (selected.trim() === '') {
|
||||
if (text.trim() === '') {
|
||||
return false;
|
||||
}
|
||||
|
||||
const quote = _.map(selected.split('\n'), val => `${`> ${val}`.trim()}\n`);
|
||||
|
||||
// If replyField already has some content, add a newline before our quote
|
||||
const separator = ($replyField.val().trim() !== '' && '\n\n') || '';
|
||||
$replyField
|
||||
.val((a, current) => `${current}${separator}${quote.join('')}\n`)
|
||||
.val((a, current) => `${current}${separator}${text}\n\n`)
|
||||
.trigger('input')
|
||||
.trigger('change');
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# Generated HTML is transformed back to GFM by app/assets/javascripts/behaviors/markdown/nodes/emoji.js
|
||||
module Banzai
|
||||
module Filter
|
||||
# HTML filter that replaces :emoji: and unicode with images.
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# Generated HTML is transformed back to GFM by app/assets/javascripts/behaviors/markdown/nodes/image.js
|
||||
module Banzai
|
||||
module Filter
|
||||
# HTML filter that moves the value of image `src` attributes to `data-src`
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# Generated HTML is transformed back to GFM by app/assets/javascripts/behaviors/markdown/nodes/image.js
|
||||
module Banzai
|
||||
module Filter
|
||||
# HTML filter that wraps links around inline images.
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# Generated HTML is transformed back to GFM by app/assets/javascripts/behaviors/markdown/marks/inline_diff.js
|
||||
module Banzai
|
||||
module Filter
|
||||
class InlineDiffFilter < HTML::Pipeline::Filter
|
||||
|
|
|
@ -2,6 +2,9 @@
|
|||
|
||||
require 'uri'
|
||||
|
||||
# Generated HTML is transformed back to GFM by:
|
||||
# - app/assets/javascripts/behaviors/markdown/marks/math.js
|
||||
# - app/assets/javascripts/behaviors/markdown/nodes/code_block.js
|
||||
module Banzai
|
||||
module Filter
|
||||
# HTML filter that adds class="code math" and removes the dollar sign in $`2+2`$.
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# Generated HTML is transformed back to GFM by app/assets/javascripts/behaviors/markdown/nodes/code_block.js
|
||||
module Banzai
|
||||
module Filter
|
||||
class MermaidFilter < HTML::Pipeline::Filter
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# Generated HTML is transformed back to GFM by app/assets/javascripts/behaviors/markdown/nodes/reference.js
|
||||
module Banzai
|
||||
module Filter
|
||||
# Base class for GitLab Flavored Markdown reference filters.
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
require 'rouge/plugins/common_mark'
|
||||
require 'rouge/plugins/redcarpet'
|
||||
|
||||
# Generated HTML is transformed back to GFM by app/assets/javascripts/behaviors/markdown/nodes/code_block.js
|
||||
module Banzai
|
||||
module Filter
|
||||
# HTML Filter to highlight fenced code blocks
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# Generated HTML is transformed back to GFM by app/assets/javascripts/behaviors/markdown/nodes/table_of_contents.js
|
||||
module Banzai
|
||||
module Filter
|
||||
# HTML filter that adds an anchor child element to all Headers in a
|
||||
|
|
|
@ -2,6 +2,10 @@
|
|||
|
||||
require 'task_list/filter'
|
||||
|
||||
# Generated HTML is transformed back to GFM by:
|
||||
# - app/assets/javascripts/behaviors/markdown/nodes/ordered_task_list.js
|
||||
# - app/assets/javascripts/behaviors/markdown/nodes/task_list.js
|
||||
# - app/assets/javascripts/behaviors/markdown/nodes/task_list_item.js
|
||||
module Banzai
|
||||
module Filter
|
||||
class TaskListFilter < TaskList::Filter
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# Generated HTML is transformed back to GFM by app/assets/javascripts/behaviors/markdown/nodes/video.js
|
||||
module Banzai
|
||||
module Filter
|
||||
# Find every image that isn't already wrapped in an `a` tag, and that has
|
||||
|
|
|
@ -3,11 +3,11 @@
|
|||
module Banzai
|
||||
module Pipeline
|
||||
class GfmPipeline < BasePipeline
|
||||
# These filters convert GitLab Flavored Markdown (GFM) to HTML.
|
||||
# The handlers defined in app/assets/javascripts/behaviors/markdown/copy_as_gfm.js
|
||||
# consequently convert that same HTML to GFM to be copied to the clipboard.
|
||||
# Every filter that generates HTML from GFM should have a handler in
|
||||
# app/assets/javascripts/behaviors/markdown/copy_as_gfm.js, in reverse order.
|
||||
# These filters transform GitLab Flavored Markdown (GFM) to HTML.
|
||||
# The nodes and marks referenced in app/assets/javascripts/behaviors/markdown/editor_extensions.js
|
||||
# consequently transform that same HTML to GFM to be copied to the clipboard.
|
||||
# Every filter that generates HTML from GFM should have a node or mark in
|
||||
# app/assets/javascripts/behaviors/markdown/editor_extensions.js.
|
||||
# The GFM-to-HTML-to-GFM cycle is tested in spec/features/copy_as_gfm_spec.rb.
|
||||
def self.filters
|
||||
@filters ||= FilterArray[
|
||||
|
|
|
@ -85,6 +85,8 @@
|
|||
"pikaday": "^1.6.1",
|
||||
"popper.js": "^1.14.3",
|
||||
"prismjs": "^1.6.0",
|
||||
"prosemirror-markdown": "^1.3.0",
|
||||
"prosemirror-model": "^1.6.4",
|
||||
"raphael": "^2.2.7",
|
||||
"raven-js": "^3.22.1",
|
||||
"raw-loader": "^1.0.0",
|
||||
|
@ -101,6 +103,9 @@
|
|||
"three-orbit-controls": "^82.1.0",
|
||||
"three-stl-loader": "^1.0.4",
|
||||
"timeago.js": "^3.0.2",
|
||||
"tiptap": "^1.8.0",
|
||||
"tiptap-commands": "^1.4.0",
|
||||
"tiptap-extensions": "^1.8.0",
|
||||
"underscore": "^1.9.0",
|
||||
"url-loader": "^1.1.2",
|
||||
"visibilityjs": "^1.2.4",
|
||||
|
|
|
@ -19,9 +19,9 @@ describe 'Copy as GFM', :js do
|
|||
visit project_issue_path(@project, @feat.issue)
|
||||
end
|
||||
|
||||
# The filters referenced in lib/banzai/pipeline/gfm_pipeline.rb convert GitLab Flavored Markdown (GFM) to HTML.
|
||||
# The handlers defined in app/assets/javascripts/behaviors/markdown/copy_as_gfm.js consequently convert that same HTML to GFM.
|
||||
# To make sure these filters and handlers are properly aligned, this spec tests the GFM-to-HTML-to-GFM cycle
|
||||
# The filters referenced in lib/banzai/pipeline/gfm_pipeline.rb transform GitLab Flavored Markdown (GFM) to HTML.
|
||||
# The nodes and marks referenced in app/assets/javascripts/behaviors/markdown/editor_extensions.js consequently transform that same HTML to GFM.
|
||||
# To make sure these filters and nodes/marks are properly aligned, this spec tests the GFM-to-HTML-to-GFM cycle
|
||||
# by verifying (`html_to_gfm(gfm_to_html(gfm)) == gfm`) for a number of examples of GFM for every filter, using the `verify` helper.
|
||||
|
||||
# These are all in a single `it` for performance reasons.
|
||||
|
@ -35,12 +35,15 @@ describe 'Copy as GFM', :js do
|
|||
verify(
|
||||
'a real world example from the gitlab-ce README',
|
||||
|
||||
<<-GFM.strip_heredoc
|
||||
<<~GFM
|
||||
# GitLab
|
||||
|
||||
[![Build status](https://gitlab.com/gitlab-org/gitlab-ce/badges/master/build.svg)](https://gitlab.com/gitlab-org/gitlab-ce/commits/master)
|
||||
|
||||
[![CE coverage report](https://gitlab.com/gitlab-org/gitlab-ce/badges/master/coverage.svg?job=coverage)](https://gitlab-org.gitlab.io/gitlab-ce/coverage-ruby)
|
||||
|
||||
[![Code Climate](https://codeclimate.com/github/gitlabhq/gitlabhq.svg)](https://codeclimate.com/github/gitlabhq/gitlabhq)
|
||||
|
||||
[![Core Infrastructure Initiative Best Practices](https://bestpractices.coreinfrastructure.org/projects/42/badge)](https://bestpractices.coreinfrastructure.org/projects/42)
|
||||
|
||||
## Canonical source
|
||||
|
@ -51,27 +54,31 @@ describe 'Copy as GFM', :js do
|
|||
|
||||
To see how GitLab looks please see the [features page on our website](https://about.gitlab.com/features/).
|
||||
|
||||
- Manage Git repositories with fine grained access controls that keep your code secure
|
||||
* Manage Git repositories with fine grained access controls that keep your code secure
|
||||
|
||||
- Perform code reviews and enhance collaboration with merge requests
|
||||
* Perform code reviews and enhance collaboration with merge requests
|
||||
|
||||
- Complete continuous integration (CI) and CD pipelines to builds, test, and deploy your applications
|
||||
* Complete continuous integration (CI) and CD pipelines to builds, test, and deploy your applications
|
||||
|
||||
- Each project can also have an issue tracker, issue board, and a wiki
|
||||
* Each project can also have an issue tracker, issue board, and a wiki
|
||||
|
||||
- Used by more than 100,000 organizations, GitLab is the most popular solution to manage Git repositories on-premises
|
||||
* Used by more than 100,000 organizations, GitLab is the most popular solution to manage Git repositories on-premises
|
||||
|
||||
- Completely free and open source (MIT Expat license)
|
||||
* Completely free and open source (MIT Expat license)
|
||||
GFM
|
||||
)
|
||||
|
||||
aggregate_failures('an accidentally selected empty element') do
|
||||
gfm = '# Heading1'
|
||||
|
||||
html = <<-HTML.strip_heredoc
|
||||
html = <<~HTML
|
||||
<h1>Heading1</h1>
|
||||
|
||||
<h2></h2>
|
||||
|
||||
<blockquote></blockquote>
|
||||
|
||||
<pre class="code highlight"></pre>
|
||||
HTML
|
||||
|
||||
output_gfm = html_to_gfm(html)
|
||||
|
@ -81,7 +88,7 @@ describe 'Copy as GFM', :js do
|
|||
aggregate_failures('an accidentally selected other element') do
|
||||
gfm = 'Test comment with **Markdown!**'
|
||||
|
||||
html = <<-HTML.strip_heredoc
|
||||
html = <<~HTML
|
||||
<li class="note">
|
||||
<div class="md">
|
||||
<p>
|
||||
|
@ -107,10 +114,17 @@ describe 'Copy as GFM', :js do
|
|||
verify(
|
||||
'TaskListFilter',
|
||||
|
||||
'- [ ] Unchecked task',
|
||||
'- [x] Checked task',
|
||||
'1. [ ] Unchecked numbered task',
|
||||
'1. [x] Checked numbered task'
|
||||
<<~GFM,
|
||||
* [ ] Unchecked task
|
||||
|
||||
* [x] Checked task
|
||||
GFM
|
||||
|
||||
<<~GFM
|
||||
1. [ ] Unchecked ordered task
|
||||
|
||||
1. [x] Checked ordered task
|
||||
GFM
|
||||
)
|
||||
|
||||
verify(
|
||||
|
@ -139,7 +153,16 @@ describe 'Copy as GFM', :js do
|
|||
verify(
|
||||
'TableOfContentsFilter',
|
||||
|
||||
'[[_TOC_]]'
|
||||
<<~GFM,
|
||||
[[_TOC_]]
|
||||
|
||||
# Heading 1
|
||||
|
||||
## Heading 2
|
||||
GFM
|
||||
|
||||
pipeline: :wiki,
|
||||
project_wiki: @project.wiki
|
||||
)
|
||||
|
||||
verify(
|
||||
|
@ -166,7 +189,7 @@ describe 'Copy as GFM', :js do
|
|||
'$`c = \pm\sqrt{a^2 + b^2}`$',
|
||||
|
||||
# math block
|
||||
<<-GFM.strip_heredoc
|
||||
<<~GFM
|
||||
```math
|
||||
c = \pm\sqrt{a^2 + b^2}
|
||||
```
|
||||
|
@ -176,7 +199,7 @@ describe 'Copy as GFM', :js do
|
|||
aggregate_failures('MathFilter: math as transformed from HTML to KaTeX') do
|
||||
gfm = '$`c = \pm\sqrt{a^2 + b^2}`$'
|
||||
|
||||
html = <<-HTML.strip_heredoc
|
||||
html = <<~HTML
|
||||
<span class="katex">
|
||||
<span class="katex-mathml">
|
||||
<math>
|
||||
|
@ -287,7 +310,7 @@ describe 'Copy as GFM', :js do
|
|||
verify(
|
||||
'MermaidFilter: mermaid as converted from GFM to HTML',
|
||||
|
||||
<<-GFM.strip_heredoc
|
||||
<<~GFM
|
||||
```mermaid
|
||||
graph TD;
|
||||
A-->B;
|
||||
|
@ -296,14 +319,14 @@ describe 'Copy as GFM', :js do
|
|||
)
|
||||
|
||||
aggregate_failures('MermaidFilter: mermaid as transformed from HTML to SVG') do
|
||||
gfm = <<-GFM.strip_heredoc
|
||||
gfm = <<~GFM
|
||||
```mermaid
|
||||
graph TD;
|
||||
A-->B;
|
||||
```
|
||||
GFM
|
||||
|
||||
html = <<-HTML.strip_heredoc
|
||||
html = <<~HTML
|
||||
<svg id="mermaidChart1" xmlns="http://www.w3.org/2000/svg" height="100%" viewBox="0 0 87.234375 174" style="max-width:87.234375px;" class="mermaid">
|
||||
<style>
|
||||
.mermaid {
|
||||
|
@ -371,8 +394,7 @@ describe 'Copy as GFM', :js do
|
|||
</g>
|
||||
</g>
|
||||
<text class="source" display="none">graph TD;
|
||||
A-->B;
|
||||
</text>
|
||||
A-->B;</text>
|
||||
</svg>
|
||||
HTML
|
||||
|
||||
|
@ -383,11 +405,18 @@ describe 'Copy as GFM', :js do
|
|||
verify(
|
||||
'SanitizationFilter',
|
||||
|
||||
<<-GFM.strip_heredoc
|
||||
<<~GFM
|
||||
<sub>sub</sub>
|
||||
|
||||
<dl>
|
||||
<dt>dt</dt>
|
||||
<dt>dt</dt>
|
||||
<dd>dd</dd>
|
||||
<dd>dd</dd>
|
||||
|
||||
<dt>dt</dt>
|
||||
<dt>dt</dt>
|
||||
<dd>dd</dd>
|
||||
<dd>dd</dd>
|
||||
</dl>
|
||||
|
||||
|
@ -399,30 +428,26 @@ describe 'Copy as GFM', :js do
|
|||
|
||||
<var>var</var>
|
||||
|
||||
<ruby>ruby</ruby>
|
||||
<abbr title="HyperText "Markup" Language">HTML</abbr>
|
||||
|
||||
<rt>rt</rt>
|
||||
<details>
|
||||
<summary>summary></summary>
|
||||
|
||||
<rp>rp</rp>
|
||||
|
||||
<abbr>abbr</abbr>
|
||||
|
||||
<summary>summary</summary>
|
||||
|
||||
<details>details</details>
|
||||
details
|
||||
</details>
|
||||
GFM
|
||||
)
|
||||
|
||||
verify(
|
||||
'SanitizationFilter',
|
||||
|
||||
<<-GFM.strip_heredoc,
|
||||
<<~GFM,
|
||||
```
|
||||
Plain text
|
||||
```
|
||||
GFM
|
||||
|
||||
<<-GFM.strip_heredoc,
|
||||
<<~GFM,
|
||||
```ruby
|
||||
def foo
|
||||
bar
|
||||
|
@ -430,11 +455,9 @@ describe 'Copy as GFM', :js do
|
|||
```
|
||||
GFM
|
||||
|
||||
<<-GFM.strip_heredoc
|
||||
<<~GFM
|
||||
Foo
|
||||
|
||||
This is an example of GFM
|
||||
|
||||
```js
|
||||
Code goes here
|
||||
```
|
||||
|
@ -452,9 +475,8 @@ describe 'Copy as GFM', :js do
|
|||
'> Quote',
|
||||
|
||||
# multiline quote
|
||||
<<-GFM.strip_heredoc,
|
||||
> Multiline
|
||||
> Quote
|
||||
<<~GFM,
|
||||
> Multiline Quote
|
||||
>
|
||||
> With multiple paragraphs
|
||||
GFM
|
||||
|
@ -465,48 +487,58 @@ describe 'Copy as GFM', :js do
|
|||
|
||||
'[Link](https://example.com)',
|
||||
|
||||
'- List item',
|
||||
<<~GFM,
|
||||
* List item
|
||||
|
||||
* List item 2
|
||||
GFM
|
||||
|
||||
# multiline list item
|
||||
<<-GFM.strip_heredoc,
|
||||
- Multiline
|
||||
List item
|
||||
<<~GFM,
|
||||
* Multiline
|
||||
|
||||
List item
|
||||
GFM
|
||||
|
||||
# nested lists
|
||||
<<-GFM.strip_heredoc,
|
||||
- Nested
|
||||
<<~GFM,
|
||||
* Nested
|
||||
|
||||
- Lists
|
||||
* Lists
|
||||
GFM
|
||||
|
||||
# list with blockquote
|
||||
<<-GFM.strip_heredoc,
|
||||
- List
|
||||
<<~GFM,
|
||||
* List
|
||||
|
||||
> Blockquote
|
||||
> Blockquote
|
||||
GFM
|
||||
|
||||
'1. Numbered list item',
|
||||
<<~GFM,
|
||||
1. Ordered list item
|
||||
|
||||
# multiline numbered list item
|
||||
<<-GFM.strip_heredoc,
|
||||
1. Ordered list item 2
|
||||
GFM
|
||||
|
||||
# multiline ordered list item
|
||||
<<~GFM,
|
||||
1. Multiline
|
||||
Numbered list item
|
||||
|
||||
Ordered list item
|
||||
GFM
|
||||
|
||||
# nested numbered list
|
||||
<<-GFM.strip_heredoc,
|
||||
# nested ordered list
|
||||
<<~GFM,
|
||||
1. Nested
|
||||
|
||||
1. Numbered lists
|
||||
1. Ordered lists
|
||||
GFM
|
||||
|
||||
# list item followed by an HR
|
||||
<<-GFM.strip_heredoc,
|
||||
- list item
|
||||
<<~GFM,
|
||||
* list item
|
||||
|
||||
-----
|
||||
---
|
||||
GFM
|
||||
|
||||
'# Heading',
|
||||
|
@ -518,14 +550,14 @@ describe 'Copy as GFM', :js do
|
|||
|
||||
'**Bold**',
|
||||
|
||||
'_Italics_',
|
||||
'*Italics*',
|
||||
|
||||
'~~Strikethrough~~',
|
||||
|
||||
'-----',
|
||||
'---',
|
||||
|
||||
# table
|
||||
<<-GFM.strip_heredoc,
|
||||
<<~GFM,
|
||||
| Centered | Right | Left |
|
||||
|:--------:|------:|------|
|
||||
| Foo | Bar | **Baz** |
|
||||
|
@ -533,9 +565,9 @@ describe 'Copy as GFM', :js do
|
|||
GFM
|
||||
|
||||
# table with empty heading
|
||||
<<-GFM.strip_heredoc,
|
||||
<<~GFM,
|
||||
| | x | y |
|
||||
|---|---|---|
|
||||
|--|---|---|
|
||||
| a | 1 | 0 |
|
||||
| b | 0 | 1 |
|
||||
GFM
|
||||
|
@ -545,9 +577,11 @@ describe 'Copy as GFM', :js do
|
|||
alias_method :gfm_to_html, :markdown
|
||||
|
||||
def verify(label, *gfms)
|
||||
markdown_options = gfms.extract_options!
|
||||
|
||||
aggregate_failures(label) do
|
||||
gfms.each do |gfm|
|
||||
html = gfm_to_html(gfm).gsub(/\A
|
\z/, '')
|
||||
html = gfm_to_html(gfm, markdown_options).gsub(/\A
|
\z/, '')
|
||||
output_gfm = html_to_gfm(html)
|
||||
expect(output_gfm.strip).to eq(gfm.strip)
|
||||
end
|
||||
|
@ -594,7 +628,7 @@ describe 'Copy as GFM', :js do
|
|||
verify(
|
||||
'[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_9"], [id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_10"]',
|
||||
|
||||
<<-GFM.strip_heredoc,
|
||||
<<~GFM,
|
||||
```ruby
|
||||
raise RuntimeError, "System commands must be given as an array of strings"
|
||||
end
|
||||
|
@ -627,7 +661,7 @@ describe 'Copy as GFM', :js do
|
|||
verify(
|
||||
'[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_8_8"], [id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_9_9"], [id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_9"], [id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_10"]',
|
||||
|
||||
<<-GFM.strip_heredoc,
|
||||
<<~GFM,
|
||||
```ruby
|
||||
unless cmd.is_a?(Array)
|
||||
raise "System commands must be given as an array of strings"
|
||||
|
@ -645,7 +679,7 @@ describe 'Copy as GFM', :js do
|
|||
verify(
|
||||
'[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_8_8"], [id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_9_9"], [id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_9"], [id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_10"]',
|
||||
|
||||
<<-GFM.strip_heredoc,
|
||||
<<~GFM,
|
||||
```ruby
|
||||
unless cmd.is_a?(Array)
|
||||
raise RuntimeError, "System commands must be given as an array of strings"
|
||||
|
@ -691,7 +725,7 @@ describe 'Copy as GFM', :js do
|
|||
verify(
|
||||
'.line[id="LC9"], .line[id="LC10"]',
|
||||
|
||||
<<-GFM.strip_heredoc,
|
||||
<<~GFM,
|
||||
```ruby
|
||||
raise RuntimeError, "System commands must be given as an array of strings"
|
||||
end
|
||||
|
@ -733,7 +767,7 @@ describe 'Copy as GFM', :js do
|
|||
verify(
|
||||
'.line[id="LC27"], .line[id="LC28"]',
|
||||
|
||||
<<-GFM.strip_heredoc,
|
||||
<<~GFM,
|
||||
```json
|
||||
"bio": null,
|
||||
"skype": "",
|
||||
|
@ -752,7 +786,7 @@ describe 'Copy as GFM', :js do
|
|||
end
|
||||
|
||||
def html_for_selector(selector)
|
||||
js = <<-JS.strip_heredoc
|
||||
js = <<~JS
|
||||
(function(selector) {
|
||||
var els = document.querySelectorAll(selector);
|
||||
var htmls = [].slice.call(els).map(function(el) { return el.outerHTML; });
|
||||
|
@ -763,7 +797,7 @@ describe 'Copy as GFM', :js do
|
|||
end
|
||||
|
||||
def html_to_gfm(html, transformer = 'transformGFMSelection', target: nil)
|
||||
js = <<-JS.strip_heredoc
|
||||
js = <<~JS
|
||||
(function(html) {
|
||||
var transformer = window.CopyAsGFM[#{transformer.inspect}];
|
||||
|
||||
|
|
|
@ -87,7 +87,7 @@ describe('CopyAsGFM', () => {
|
|||
spyOn(window, 'getSelection').and.returnValue(selection);
|
||||
simulateCopy();
|
||||
|
||||
const expectedGFM = '- List Item1\n- List Item2';
|
||||
const expectedGFM = '* List Item1\n\n* List Item2';
|
||||
|
||||
expect(clipboardData.setData).toHaveBeenCalledWith('text/x-gfm', expectedGFM);
|
||||
});
|
||||
|
@ -97,7 +97,7 @@ describe('CopyAsGFM', () => {
|
|||
spyOn(window, 'getSelection').and.returnValue(selection);
|
||||
simulateCopy();
|
||||
|
||||
const expectedGFM = '1. List Item1\n1. List Item2';
|
||||
const expectedGFM = '1. List Item1\n\n1. List Item2';
|
||||
|
||||
expect(clipboardData.setData).toHaveBeenCalledWith('text/x-gfm', expectedGFM);
|
||||
});
|
||||
|
|
|
@ -186,7 +186,7 @@ describe('ShortcutsIssuable', function() {
|
|||
it('adds the quoted selection to the input', () => {
|
||||
ShortcutsIssuable.replyWithSelectedText(true);
|
||||
|
||||
expect($(FORM_SELECTOR).val()).toBe('> _Selected text._\n\n');
|
||||
expect($(FORM_SELECTOR).val()).toBe('> *Selected text.*\n\n');
|
||||
});
|
||||
|
||||
it('triggers `focus`', () => {
|
||||
|
|
232
yarn.lock
232
yarn.lock
|
@ -4102,6 +4102,13 @@ fastparse@^1.1.1:
|
|||
resolved "https://registry.yarnpkg.com/fastparse/-/fastparse-1.1.1.tgz#d1e2643b38a94d7583b479060e6c4affc94071f8"
|
||||
integrity sha1-0eJkOzipTXWDtHkGDmxK/8lAcfg=
|
||||
|
||||
fault@^1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/fault/-/fault-1.0.2.tgz#c3d0fec202f172a3a4d414042ad2bb5e2a3ffbaa"
|
||||
integrity sha512-o2eo/X2syzzERAtN5LcGbiVQ0WwZSlN3qLtadwAz3X8Bu+XWD16dja/KMsjZLiQr+BLGPDnHGkc4yUJf1Xpkpw==
|
||||
dependencies:
|
||||
format "^0.2.2"
|
||||
|
||||
faye-websocket@^0.10.0:
|
||||
version "0.10.0"
|
||||
resolved "https://registry.yarnpkg.com/faye-websocket/-/faye-websocket-0.10.0.tgz#4e492f8d04dfb6f89003507f6edbf2d501e7c6f4"
|
||||
|
@ -4321,6 +4328,11 @@ form-data@~2.3.2:
|
|||
combined-stream "^1.0.6"
|
||||
mime-types "^2.1.12"
|
||||
|
||||
format@^0.2.2:
|
||||
version "0.2.2"
|
||||
resolved "https://registry.yarnpkg.com/format/-/format-0.2.2.tgz#d6170107e9efdc4ed30c9dc39016df942b5cb58b"
|
||||
integrity sha1-1hcBB+nv3E7TDJ3DkBbflCtctYs=
|
||||
|
||||
formdata-polyfill@^3.0.11:
|
||||
version "3.0.11"
|
||||
resolved "https://registry.yarnpkg.com/formdata-polyfill/-/formdata-polyfill-3.0.11.tgz#c82b4b4bea3356c0a6752219e54ce1edb2a7fb5b"
|
||||
|
@ -4819,7 +4831,7 @@ he@^1.1.0, he@^1.1.1:
|
|||
resolved "https://registry.yarnpkg.com/he/-/he-1.1.1.tgz#93410fd21b009735151f8868c2f271f3427e23fd"
|
||||
integrity sha1-k0EP0hsAlzUVH4howvJx80J+I/0=
|
||||
|
||||
highlight.js@^9.13.1:
|
||||
highlight.js@^9.13.1, highlight.js@~9.13.0:
|
||||
version "9.13.1"
|
||||
resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-9.13.1.tgz#054586d53a6863311168488a0f58d6c505ce641e"
|
||||
integrity sha512-Sc28JNQNDzaH6PORtRLMvif9RSn1mYuOoX3omVjnb0+HbpPygU2ALBI0R/wsiqCb4/fcp07Gdo8g+fhtFrQl6A==
|
||||
|
@ -6470,6 +6482,13 @@ lightercollective@^0.1.0:
|
|||
resolved "https://registry.yarnpkg.com/lightercollective/-/lightercollective-0.1.0.tgz#70df102c530dcb8d0ccabfe6175a8d00d5f61300"
|
||||
integrity sha512-J9tg5uraYoQKaWbmrzDDexbG6hHnMcWS1qLYgJSWE+mpA3U5OCSeMUhb+K55otgZJ34oFdR0ECvdIb3xuO5JOQ==
|
||||
|
||||
linkify-it@^2.0.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/linkify-it/-/linkify-it-2.1.0.tgz#c4caf38a6cd7ac2212ef3c7d2bde30a91561f9db"
|
||||
integrity sha512-4REs8/062kV2DSHxNfq5183zrqXMl7WP0WzABH9IeJI+NLm429FgE1PDecltYfnOoFDFlZGh2T8PfZn0r+GTRg==
|
||||
dependencies:
|
||||
uc.micro "^1.0.1"
|
||||
|
||||
load-json-file@^1.0.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-1.1.0.tgz#956905708d58b4bab4c2261b04f59f31c99374c0"
|
||||
|
@ -6631,6 +6650,14 @@ lowercase-keys@1.0.0, lowercase-keys@^1.0.0:
|
|||
resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-1.0.0.tgz#4e3366b39e7f5457e35f1324bdf6f88d0bfc7306"
|
||||
integrity sha1-TjNms55/VFfjXxMkvfb4jQv8cwY=
|
||||
|
||||
lowlight@^1.11.0:
|
||||
version "1.11.0"
|
||||
resolved "https://registry.yarnpkg.com/lowlight/-/lowlight-1.11.0.tgz#1304d83005126d4e8b1dc0f07981e9b689ec2efc"
|
||||
integrity sha512-xrGGN6XLL7MbTMdPD6NfWPwY43SNkjf/d0mecSx/CW36fUZTjRHEq0/Cdug3TWKtRXLWi7iMl1eP0olYxj/a4A==
|
||||
dependencies:
|
||||
fault "^1.0.2"
|
||||
highlight.js "~9.13.0"
|
||||
|
||||
lru-cache@2.2.x:
|
||||
version "2.2.4"
|
||||
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-2.2.4.tgz#6c658619becf14031d0d0b594b16042ce4dc063d"
|
||||
|
@ -6689,6 +6716,17 @@ map-visit@^1.0.0:
|
|||
dependencies:
|
||||
object-visit "^1.0.0"
|
||||
|
||||
markdown-it@^8.4.2:
|
||||
version "8.4.2"
|
||||
resolved "https://registry.yarnpkg.com/markdown-it/-/markdown-it-8.4.2.tgz#386f98998dc15a37722aa7722084f4020bdd9b54"
|
||||
integrity sha512-GcRz3AWTqSUphY3vsUqQSFMbgR38a4Lh3GWlHRh/7MRwz8mcu9n2IO7HOh+bXHrR9kOPDl5RNCaEsrneb+xhHQ==
|
||||
dependencies:
|
||||
argparse "^1.0.7"
|
||||
entities "~1.1.1"
|
||||
linkify-it "^2.0.0"
|
||||
mdurl "^1.0.1"
|
||||
uc.micro "^1.0.5"
|
||||
|
||||
marked@^0.3.12, marked@~0.3.6:
|
||||
version "0.3.19"
|
||||
resolved "https://registry.yarnpkg.com/marked/-/marked-0.3.19.tgz#5d47f709c4c9fc3c216b6d46127280f40b39d790"
|
||||
|
@ -6707,6 +6745,11 @@ md5.js@^1.3.4:
|
|||
hash-base "^3.0.0"
|
||||
inherits "^2.0.1"
|
||||
|
||||
mdurl@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/mdurl/-/mdurl-1.0.1.tgz#fe85b2ec75a59037f2adfec100fd6c601761152e"
|
||||
integrity sha1-/oWy7HWlkDfyrf7BAP1sYBdhFS4=
|
||||
|
||||
media-typer@0.3.0:
|
||||
version "0.3.0"
|
||||
resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748"
|
||||
|
@ -7420,6 +7463,11 @@ optionator@^0.8.1, optionator@^0.8.2:
|
|||
type-check "~0.3.2"
|
||||
wordwrap "~1.0.0"
|
||||
|
||||
orderedmap@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/orderedmap/-/orderedmap-1.0.0.tgz#d90fc2ba1ed085190907d601dec6e6a53f8d41ba"
|
||||
integrity sha1-2Q/Cuh7QhRkJB9YB3sbmpT+NQbo=
|
||||
|
||||
original@^1.0.0:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/original/-/original-1.0.2.tgz#e442a61cffe1c5fd20a65f3261c26663b303f25f"
|
||||
|
@ -7956,6 +8004,122 @@ prompts@^0.1.9:
|
|||
kleur "^2.0.1"
|
||||
sisteransi "^0.1.1"
|
||||
|
||||
prosemirror-commands@^1.0.7:
|
||||
version "1.0.7"
|
||||
resolved "https://registry.yarnpkg.com/prosemirror-commands/-/prosemirror-commands-1.0.7.tgz#e5a2ba821e29ea7065c88277fe2c3d7f6b0b9d37"
|
||||
integrity sha512-IR8yMSdw7XlKuF68tydAak1J9P/lLD5ohsrL7pzoLsJAJAQU7mVPDXtGbQrrm0mesddFjcc1zNo/cJQN3lRYnA==
|
||||
dependencies:
|
||||
prosemirror-model "^1.0.0"
|
||||
prosemirror-state "^1.0.0"
|
||||
prosemirror-transform "^1.0.0"
|
||||
|
||||
prosemirror-dropcursor@^1.1.1:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/prosemirror-dropcursor/-/prosemirror-dropcursor-1.1.1.tgz#c60ed1ed6c58804a06a75db06a0d993b087b7622"
|
||||
integrity sha512-GeUyMO/tOEf8MXrP7Xb7UIMrfK86OGh0fnyBrHfhav4VjY9cw65mNoqHy87CklE5711AhCP5Qzfp8RL/hVKusg==
|
||||
dependencies:
|
||||
prosemirror-state "^1.0.0"
|
||||
prosemirror-transform "^1.1.0"
|
||||
prosemirror-view "^1.1.0"
|
||||
|
||||
prosemirror-gapcursor@^1.0.3:
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/prosemirror-gapcursor/-/prosemirror-gapcursor-1.0.3.tgz#acc6537fc5a35e9b38966f91a199a382dfc715c4"
|
||||
integrity sha512-X+hJhr42PcHWiSWL+lI5f/UeOhXCxlBFb8M6O8aG1hssmaRrW7sS2/Fjg5jFV+pTdS1REFkmm1occh01FMdDIQ==
|
||||
dependencies:
|
||||
prosemirror-keymap "^1.0.0"
|
||||
prosemirror-model "^1.0.0"
|
||||
prosemirror-state "^1.0.0"
|
||||
prosemirror-view "^1.0.0"
|
||||
|
||||
prosemirror-history@^1.0.3:
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/prosemirror-history/-/prosemirror-history-1.0.3.tgz#5fb8591adfc272afaaf0b41bec64ee7d9522a118"
|
||||
integrity sha512-IfFGbhafSx+R3aq7nLJGkXeu2iaUiP8mkU3aRu2uQcIIjU8Fq7RJfuvhIOJ2RNUoSyqF/ANkdTjnZ74F5eHs1Q==
|
||||
dependencies:
|
||||
prosemirror-state "^1.2.2"
|
||||
prosemirror-transform "^1.0.0"
|
||||
rope-sequence "^1.2.0"
|
||||
|
||||
prosemirror-inputrules@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/prosemirror-inputrules/-/prosemirror-inputrules-1.0.1.tgz#f63305fd966379f218e82ca76a2a9b328b66dc7b"
|
||||
integrity sha512-UHy22NmwxS5WIMQYkzraDttQAF8mpP82FfbJsmKFfx6jwkR/SZa+ZhbkLY0zKQ5fBdJN7euj36JG/B5iAlrpxA==
|
||||
dependencies:
|
||||
prosemirror-state "^1.0.0"
|
||||
prosemirror-transform "^1.0.0"
|
||||
|
||||
prosemirror-keymap@^1.0.0, prosemirror-keymap@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/prosemirror-keymap/-/prosemirror-keymap-1.0.1.tgz#03ef32b828e3a859dfb570eb84928bf2e5330bc2"
|
||||
integrity sha512-e79ApE7PXXZMFtPz7WbjycjAFd1NPjgY1MkecVz98tqwlBSggXWXYQnWFk6x7UkmnBYRHHbXHkR/RXmu2wyBJg==
|
||||
dependencies:
|
||||
prosemirror-state "^1.0.0"
|
||||
w3c-keyname "^1.1.8"
|
||||
|
||||
prosemirror-markdown@^1.3.0:
|
||||
version "1.3.0"
|
||||
resolved "https://registry.yarnpkg.com/prosemirror-markdown/-/prosemirror-markdown-1.3.0.tgz#a100d14c27da7d8fb70818230d786898eeadb7fa"
|
||||
integrity sha512-76l3yLB/suy6sA7LpzRJvRRWkHtKwOTpgWVNwmlIAIIZJeMypWSPldT/gFyIG604eyXEPZitnx+j80Y2DpbnUQ==
|
||||
dependencies:
|
||||
markdown-it "^8.4.2"
|
||||
prosemirror-model "^1.0.0"
|
||||
|
||||
prosemirror-model@^1.0.0, prosemirror-model@^1.1.0, prosemirror-model@^1.6.4:
|
||||
version "1.6.4"
|
||||
resolved "https://registry.yarnpkg.com/prosemirror-model/-/prosemirror-model-1.6.4.tgz#2ac37a629448a7dbfd1635450e2fdd63c3450d7d"
|
||||
integrity sha512-C2ALle8fZsAza+6stUF9Gv28jH9XtpNeczb33bowGlnb2cpNI4FZf1HHUyZjf6ou4cEvOlbt6fAYsT4NCKmlcQ==
|
||||
dependencies:
|
||||
orderedmap "^1.0.0"
|
||||
|
||||
prosemirror-schema-list@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/prosemirror-schema-list/-/prosemirror-schema-list-1.0.1.tgz#f216e0cf4809b6074aa27912449ac89897f1ae94"
|
||||
integrity sha512-AiLIX6qm6PEeDtMCKZLcSLi55WXo1ls7DnRK+4hSkoi0IIzNdxGsRlecCd3MzEu//DVz3nAEh+zEmslyW+uk8g==
|
||||
dependencies:
|
||||
prosemirror-model "^1.0.0"
|
||||
prosemirror-transform "^1.0.0"
|
||||
|
||||
prosemirror-state@^1.0.0, prosemirror-state@^1.2.1, prosemirror-state@^1.2.2:
|
||||
version "1.2.2"
|
||||
resolved "https://registry.yarnpkg.com/prosemirror-state/-/prosemirror-state-1.2.2.tgz#8df26d95fd6fd327c0f9984a760e84d863204154"
|
||||
integrity sha512-j8aC/kf9BJSCQau485I/9pj39XQoce+TqH5xzekT7WWFARTsRYFLJtiXBcCKakv1VSeev+sC3bJP0pLfz7Ft8g==
|
||||
dependencies:
|
||||
prosemirror-model "^1.0.0"
|
||||
prosemirror-transform "^1.0.0"
|
||||
|
||||
prosemirror-tables@^0.7.10, prosemirror-tables@^0.7.9:
|
||||
version "0.7.10"
|
||||
resolved "https://registry.yarnpkg.com/prosemirror-tables/-/prosemirror-tables-0.7.10.tgz#4b0f623422b4b8f84cdc9c559f8a87579846b3ba"
|
||||
integrity sha512-VIu7UGS9keYEHs0Y6AEOTGbNE9QI2rL1OKng4vV6yoTshW/lYcb+s3hGXI12i+WLMjDVm7ujhfdWrpKpvFZOkQ==
|
||||
dependencies:
|
||||
prosemirror-keymap "^1.0.0"
|
||||
prosemirror-model "^1.0.0"
|
||||
prosemirror-state "^1.0.0"
|
||||
prosemirror-transform "^1.0.0"
|
||||
prosemirror-view "^1.0.0"
|
||||
|
||||
prosemirror-transform@^1.0.0, prosemirror-transform@^1.1.0:
|
||||
version "1.1.3"
|
||||
resolved "https://registry.yarnpkg.com/prosemirror-transform/-/prosemirror-transform-1.1.3.tgz#28cfdf1f9ee514edc40466be7b7db39eed545fdf"
|
||||
integrity sha512-1O6Di5lOL1mp4nuCnQNkHY7l2roIW5y8RH4ZG3hMYmkmDEWzTaFFnxxAAHsE5ipGLBSRcTlP7SsDhYBIdSuLpQ==
|
||||
dependencies:
|
||||
prosemirror-model "^1.0.0"
|
||||
|
||||
prosemirror-utils@^0.7.5:
|
||||
version "0.7.5"
|
||||
resolved "https://registry.yarnpkg.com/prosemirror-utils/-/prosemirror-utils-0.7.5.tgz#11b477647b672ec8f10679ab298a5823dad6457a"
|
||||
integrity sha512-F+63BUiBkUQb1S07c3rGHXjE4MDaZ5OjsNhmaO7eDdSh1lUNORTJJHrvlFEZKnLM7ChoDDXTIKhWNQwnCssQfA==
|
||||
|
||||
prosemirror-view@^1.0.0, prosemirror-view@^1.1.0, prosemirror-view@^1.6.8:
|
||||
version "1.6.8"
|
||||
resolved "https://registry.yarnpkg.com/prosemirror-view/-/prosemirror-view-1.6.8.tgz#33fc1a6e2731633e5d6dc1af1967378f15810b74"
|
||||
integrity sha512-YWX3rfji77xsU5EErt4ZoecVytYW9/4oHBYhV1MUHGMYIcppe+QZEBgRlyPMBUuu0lxdZX4m3sq7fCsDvv/MlQ==
|
||||
dependencies:
|
||||
prosemirror-model "^1.1.0"
|
||||
prosemirror-state "^1.0.0"
|
||||
prosemirror-transform "^1.1.0"
|
||||
|
||||
proto-list@~1.2.1:
|
||||
version "1.2.4"
|
||||
resolved "https://registry.yarnpkg.com/proto-list/-/proto-list-1.2.4.tgz#212d5bfe1318306a420f6402b8e26ff39647a849"
|
||||
|
@ -8549,6 +8713,11 @@ ripemd160@^2.0.0, ripemd160@^2.0.1:
|
|||
hash-base "^2.0.0"
|
||||
inherits "^2.0.1"
|
||||
|
||||
rope-sequence@^1.2.0:
|
||||
version "1.2.2"
|
||||
resolved "https://registry.yarnpkg.com/rope-sequence/-/rope-sequence-1.2.2.tgz#49c4e5c2f54a48e990b050926771e2871bcb31ce"
|
||||
integrity sha1-ScTlwvVKSOmQsFCSZ3HihxvLMc4=
|
||||
|
||||
rsvp@^3.3.3:
|
||||
version "3.6.2"
|
||||
resolved "https://registry.yarnpkg.com/rsvp/-/rsvp-3.6.2.tgz#2e96491599a96cde1b515d5674a8f7a91452926a"
|
||||
|
@ -9497,6 +9666,57 @@ tiny-emitter@^2.0.0:
|
|||
resolved "https://registry.yarnpkg.com/tiny-emitter/-/tiny-emitter-2.0.2.tgz#82d27468aca5ade8e5fd1e6d22b57dd43ebdfb7c"
|
||||
integrity sha512-2NM0auVBGft5tee/OxP4PI3d8WItkDM+fPnaRAVo6xTDI2knbz9eC5ArWGqtGlYqiH3RU5yMpdyTTO7MguC4ow==
|
||||
|
||||
tiptap-commands@^1.4.0:
|
||||
version "1.4.0"
|
||||
resolved "https://registry.yarnpkg.com/tiptap-commands/-/tiptap-commands-1.4.0.tgz#0cfb3ac138ee3099de56114cb119abd841fbcbe7"
|
||||
integrity sha512-ytO8jFXgufK5DziamTaVojzUTolWvL4m2xNXaLkAVJYy9CWXruMK7avqeLoFYPI4GZlhleMn5i4gzYTbD7e2jA==
|
||||
dependencies:
|
||||
prosemirror-commands "^1.0.7"
|
||||
prosemirror-inputrules "^1.0.1"
|
||||
prosemirror-schema-list "^1.0.1"
|
||||
prosemirror-state "^1.2.2"
|
||||
tiptap-utils "^1.1.1"
|
||||
|
||||
tiptap-extensions@^1.8.0:
|
||||
version "1.8.0"
|
||||
resolved "https://registry.yarnpkg.com/tiptap-extensions/-/tiptap-extensions-1.8.0.tgz#3067620a024f1a9e5fae4450790b143d7ebe4394"
|
||||
integrity sha512-1JN9uk5QnA7DTID1j07gIBEqeOnRd6lwZ5rx/zqWXJLyreZu8VDPvP939tfP41GskO4oicGlhmsQ0aEnA5QYDw==
|
||||
dependencies:
|
||||
lowlight "^1.11.0"
|
||||
prosemirror-history "^1.0.3"
|
||||
prosemirror-state "^1.2.2"
|
||||
prosemirror-tables "^0.7.10"
|
||||
prosemirror-utils "^0.7.5"
|
||||
prosemirror-view "^1.6.8"
|
||||
tiptap "^1.8.0"
|
||||
tiptap-commands "^1.4.0"
|
||||
|
||||
tiptap-utils@^1.1.1:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/tiptap-utils/-/tiptap-utils-1.1.1.tgz#e7aad3e84eb35f7abed704d15da0420029789d0d"
|
||||
integrity sha512-yPIWwLFaL5a0GC7fcO7aoPlASnH3wOUQex0IlepNWbDCNycSL8shXhVx0HMN/tCnlp943zw1bwcYzpTW3wA4tw==
|
||||
dependencies:
|
||||
prosemirror-model "^1.6.4"
|
||||
prosemirror-state "^1.2.2"
|
||||
prosemirror-tables "^0.7.9"
|
||||
prosemirror-utils "^0.7.5"
|
||||
|
||||
tiptap@^1.8.0:
|
||||
version "1.8.0"
|
||||
resolved "https://registry.yarnpkg.com/tiptap/-/tiptap-1.8.0.tgz#c671188075ffa5ee4f86470f95818fd9ce6f1040"
|
||||
integrity sha512-zIcVY8U1Wgj4bg3R4pX5a2BCpZUw/dTCh259VZ9g5MtClnzdLW2XpKCcwqfa9iUBEs6MCPSnB3t8jGRtGciHJg==
|
||||
dependencies:
|
||||
prosemirror-commands "^1.0.7"
|
||||
prosemirror-dropcursor "^1.1.1"
|
||||
prosemirror-gapcursor "^1.0.3"
|
||||
prosemirror-inputrules "^1.0.1"
|
||||
prosemirror-keymap "^1.0.1"
|
||||
prosemirror-model "^1.6.4"
|
||||
prosemirror-state "^1.2.1"
|
||||
prosemirror-view "^1.6.8"
|
||||
tiptap-commands "^1.4.0"
|
||||
tiptap-utils "^1.1.1"
|
||||
|
||||
tmp@0.0.33, tmp@0.0.x, tmp@^0.0.33:
|
||||
version "0.0.33"
|
||||
resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9"
|
||||
|
@ -9648,6 +9868,11 @@ typescript@^2:
|
|||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.9.2.tgz#1cbf61d05d6b96269244eb6a3bce4bd914e0f00c"
|
||||
integrity sha512-Gr4p6nFNaoufRIY4NMdpQRNmgxVIGMs4Fcu/ujdYk3nAZqk7supzBE9idmvfZIlH/Cuj//dvi+019qEue9lV0w==
|
||||
|
||||
uc.micro@^1.0.1, uc.micro@^1.0.5:
|
||||
version "1.0.5"
|
||||
resolved "https://registry.yarnpkg.com/uc.micro/-/uc.micro-1.0.5.tgz#0c65f15f815aa08b560a61ce8b4db7ffc3f45376"
|
||||
integrity sha512-JoLI4g5zv5qNyT09f4YAvEZIIV1oOjqnewYg5D38dkQljIzpPT296dbIGvKro3digYI1bkb7W6EP1y4uDlmzLg==
|
||||
|
||||
uglify-js@^3.1.4:
|
||||
version "3.4.9"
|
||||
resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.4.9.tgz#af02f180c1207d76432e473ed24a28f4a782bae3"
|
||||
|
@ -10052,6 +10277,11 @@ w3c-hr-time@^1.0.1:
|
|||
dependencies:
|
||||
browser-process-hrtime "^0.1.2"
|
||||
|
||||
w3c-keyname@^1.1.8:
|
||||
version "1.1.8"
|
||||
resolved "https://registry.yarnpkg.com/w3c-keyname/-/w3c-keyname-1.1.8.tgz#4e2219663760fd6535b7a1550f1552d71fc9372c"
|
||||
integrity sha512-2HAdug8GTiu3b4NYhssdtY8PXRue3ICnh1IlxvZYl+hiINRq0GfNWei3XOPDg8L0PsxbmYjWVLuLj6BMRR/9vA==
|
||||
|
||||
walker@~1.0.5:
|
||||
version "1.0.7"
|
||||
resolved "https://registry.yarnpkg.com/walker/-/walker-1.0.7.tgz#2f7f9b8fd10d677262b18a884e28d19618e028fb"
|
||||
|
|
Loading…
Reference in New Issue