From 94949bc635938827474af6d61418860aa2a1221f Mon Sep 17 00:00:00 2001 From: Max Date: Mon, 29 Jan 2024 11:25:09 +0100 Subject: [PATCH 01/12] enh(nodes): introduce preview node Use title attribute to mark previews in markdown. Introduce PreviewOptions component to add preview options for isolated links. If there's just one link in a paragraph you can turn it into a preview. Signed-off-by: Max --- sample.md | 2 + src/components/Editor/PreviewOptions.vue | 75 ++++++++++++++++++ src/extensions/RichText.js | 6 +- src/markdownit/index.js | 4 +- src/markdownit/preview.js | 62 +++++++++++++++ src/marks/Link.js | 1 - src/nodes/ParagraphView.vue | 49 ++++++------ src/nodes/Preview.js | 91 ++++++++++++++++++++++ src/nodes/Preview.vue | 96 ++++++++++++++++++++++++ src/tests/markdown.spec.js | 10 +++ src/tests/markdownit/preview.spec.js | 63 ++++++++++++++++ src/tests/nodes/Preview.spec.js | 45 +++++++++++ 12 files changed, 479 insertions(+), 25 deletions(-) create mode 100644 sample.md create mode 100644 src/components/Editor/PreviewOptions.vue create mode 100644 src/markdownit/preview.js create mode 100644 src/nodes/Preview.js create mode 100644 src/nodes/Preview.vue create mode 100644 src/tests/markdownit/preview.spec.js create mode 100644 src/tests/nodes/Preview.spec.js diff --git a/sample.md b/sample.md new file mode 100644 index 00000000000..4c27b77bb71 --- /dev/null +++ b/sample.md @@ -0,0 +1,2 @@ +[Test me](https://www.nextcloud.com (preview)) + diff --git a/src/components/Editor/PreviewOptions.vue b/src/components/Editor/PreviewOptions.vue new file mode 100644 index 00000000000..8e19892d2eb --- /dev/null +++ b/src/components/Editor/PreviewOptions.vue @@ -0,0 +1,75 @@ + + + + + + diff --git a/src/extensions/RichText.js b/src/extensions/RichText.js index 7ae707f3f1e..48637013d10 100644 --- a/src/extensions/RichText.js +++ b/src/extensions/RichText.js @@ -26,7 +26,7 @@ import { lowlight } from 'lowlight' /* eslint-disable import/no-named-as-default */ import Blockquote from '@tiptap/extension-blockquote' import BulletList from './../nodes/BulletList.js' -import Callout from './../nodes/Callouts.js' +import Callouts from './../nodes/Callouts.js' import CharacterCount from '@tiptap/extension-character-count' import Code from '@tiptap/extension-code' import CodeBlock from './../nodes/CodeBlock.js' @@ -51,6 +51,7 @@ import Mention from './../extensions/Mention.js' import OrderedList from '@tiptap/extension-ordered-list' import Paragraph from './../nodes/Paragraph.js' import Placeholder from '@tiptap/extension-placeholder' +import Preview from './../nodes/Preview.js' import Table from './../nodes/Table.js' import TaskItem from './../nodes/TaskItem.js' import TaskList from './../nodes/TaskList.js' @@ -100,7 +101,8 @@ export default Extension.create({ this.options.editing ? EditableTable : Table, TaskList, TaskItem, - Callout, + Callouts, + Preview, Underline, Image, ImageInline, diff --git a/src/markdownit/index.js b/src/markdownit/index.js index 065237b4e6a..baa45d37131 100644 --- a/src/markdownit/index.js +++ b/src/markdownit/index.js @@ -4,6 +4,7 @@ import markdownitMentions from '@quartzy/markdown-it-mentions' import underline from './underline.js' import splitMixedLists from './splitMixedLists.js' import callouts from './callouts.js' +import preview from './preview.js' import hardbreak from './hardbreak.js' import keepSyntax from './keepSyntax.js' import frontMatter from 'markdown-it-front-matter' @@ -15,10 +16,11 @@ const markdownit = MarkdownIt('commonmark', { html: false, breaks: false }) .enable('table') .use(taskLists, { enable: true, labelAfter: true }) .use(frontMatter, (fm) => {}) - .use(splitMixedLists) + .use(splitMixedLists) // needs task Lists to be used first. .use(underline) .use(hardbreak) .use(callouts) + .use(preview) .use(keepSyntax) .use(markdownitMentions) .use(implicitFigures) diff --git a/src/markdownit/preview.js b/src/markdownit/preview.js new file mode 100644 index 00000000000..559839b3751 --- /dev/null +++ b/src/markdownit/preview.js @@ -0,0 +1,62 @@ +/** + * @copyright Copyright (c) 2024 Max + * + * @author Max + * + * @license AGPL-3.0-or-later + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +function isPreviewLinkInParagraph(tokens, i) { + const [prev, cur, next] = tokens.slice(i-1, i+2) + return prev.type === 'paragraph_open' + && cur.type === 'inline' + && cur.children + && cur.children.length === 3 + && cur.children[0].type === 'link_open' + && cur.children[0].attrGet('title') === 'preview' + && cur.children[1].type === 'text' + && cur.children[2].type === 'link_close' + && next.type === 'paragraph_close' +} + +/* Remove wrapping tokens + * + * @param {array} tokens - the token stream to modify + * @param {Number} i - index of the token to unwrap + */ +function unwrapToken(tokens, i) { + // Start from the end so indexes stay the same. + tokens.splice(i+1, 1) + tokens.splice(i-1, 1) +} + +/** + * @param {object} md Markdown object + */ +export default (md) => { + + function linkPreviews({ tokens }) { + // do not process first and last token + for (let i = 1, l = tokens.length; i < (l - 1); ++i) { + if (isPreviewLinkInParagraph(tokens, i)) { + unwrapToken(tokens, i) + } + } + } + + md.core.ruler.before('linkify', 'link_previews', linkPreviews); +} diff --git a/src/marks/Link.js b/src/marks/Link.js index 84cedc89085..9faebc9f8e4 100644 --- a/src/marks/Link.js +++ b/src/marks/Link.js @@ -58,7 +58,6 @@ const Link = TipTapLink.extend({ renderHTML(options) { const { mark } = options - return ['a', { ...mark.attrs, href: domHref(mark, this.options.relativePath), diff --git a/src/nodes/ParagraphView.vue b/src/nodes/ParagraphView.vue index b1b7db2fba4..04d32b3d3b2 100644 --- a/src/nodes/ParagraphView.vue +++ b/src/nodes/ParagraphView.vue @@ -22,19 +22,18 @@ diff --git a/src/nodes/Preview.js b/src/nodes/Preview.js new file mode 100644 index 00000000000..2401633d418 --- /dev/null +++ b/src/nodes/Preview.js @@ -0,0 +1,91 @@ +/* + * @copyright Copyright (c) 2022 Vinicius Reis + * + * @author Vinicius Reis + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +import { Node, isNodeActive } from '@tiptap/core' +import { domHref, parseHref } from './../helpers/links.js' +import { VueNodeViewRenderer } from '@tiptap/vue-2' + +import Preview from './Preview.vue' + +export default Node.create({ + + name: 'preview', + + group: 'block', + + content: 'text?', + + defining: true, + + addOptions() { + return { + relativePath: null, + } + }, + + addAttributes() { + return { + href: { parseHTML: parseHref }, + title: { parseHTML: el => el.getAttribute('title') }, + } + }, + + parseHTML() { + return [ + { + tag: 'a[title="preview"]', + priority: 1001, + }, + ] + }, + + renderHTML({ node }) { + return ['a', { + ...node.attrs, + href: domHref(node, this.options.relativePath), + rel: 'noopener noreferrer nofollow', + }, 0] + }, + + addNodeView() { + return VueNodeViewRenderer(Preview) + }, + + toMarkdown: (state, node) => { + state.write(`[`) + state.text(node.textContent, false) + state.write(`](${node.attrs.href} (${node.attrs.title}))`) + }, + + addCommands() { + return { + setPreview: attrs => ({ commands }) => { + const attributes = { ...attrs, title: 'preview' } + return commands.setNode(this.name, attributes) + }, + togglePreview: attrs => ({ commands }) => { + const attributes = { ...attrs, title: 'preview' } + return commands.toggleNode(this.name, 'paragraph', attributes) + }, + } + }, +}) diff --git a/src/nodes/Preview.vue b/src/nodes/Preview.vue new file mode 100644 index 00000000000..1c90d69a8e0 --- /dev/null +++ b/src/nodes/Preview.vue @@ -0,0 +1,96 @@ + + + + + + diff --git a/src/tests/markdown.spec.js b/src/tests/markdown.spec.js index f59f9aabb26..008cb6bae3b 100644 --- a/src/tests/markdown.spec.js +++ b/src/tests/markdown.spec.js @@ -103,6 +103,16 @@ describe('Markdown though editor', () => { }) }) + test('preview with url only', () => { + const entry = '::: preview\nhttps://nextcloud.com\n\n:::' + expect(markdownThroughEditor(entry)).toBe(entry) + }) + + test('preview with text', () => { + const entry = '::: preview\n[text](https://nextcloud.com)\n\n:::' + expect(markdownThroughEditor(entry)).toBe(entry) + }) + test('front matter', () => { expect(markdownThroughEditor('---\nhello: world\n---')).toBe('---\nhello: world\n---') expect(markdownThroughEditor('---\n---')).toBe('---\n---') diff --git a/src/tests/markdownit/preview.spec.js b/src/tests/markdownit/preview.spec.js new file mode 100644 index 00000000000..bc6de3de596 --- /dev/null +++ b/src/tests/markdownit/preview.spec.js @@ -0,0 +1,63 @@ +/** + * @copyright Copyright (c) 2024 Max + * + * @author Max + * + * @license AGPL-3.0-or-later + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +import markdownit from '../../markdownit' +import stripIndent from './stripIndent.js' + +describe('Preview extension', () => { + + const link = { + md: `[link](https://nextcloud.com)`, + html: `link`, + } + const preview = { + md: `[link](https://nextcloud.com (preview))`, + html: `link`, + } + + it('wraps', () => { + expect(markdownit.render('[link](https://nextcloud.com)')) + .toBe( + `

link

\n` + ) + }) + + it(`unwraps preview from paragraph`, () => { + const rendered = markdownit.render(preview.md) + expect(rendered).toBe(preview.html) + }) + + it(`leaves non-preview links alone`, () => { + const rendered = markdownit.render(link.md) + expect(rendered).toBe( + `

${link.html}

\n` + ) + }) + + it(`leaves two previews in one paragraph`, () => { + const rendered = markdownit.render(`${preview.md}\n${preview.md}`) + expect(rendered).toBe( + `

${preview.html}\n${preview.html}

\n` + ) + }) + +}) diff --git a/src/tests/nodes/Preview.spec.js b/src/tests/nodes/Preview.spec.js new file mode 100644 index 00000000000..8b401996975 --- /dev/null +++ b/src/tests/nodes/Preview.spec.js @@ -0,0 +1,45 @@ +import Preview from './../../nodes/Preview' +import Markdown from './../../extensions/Markdown' +import { getExtensionField } from '@tiptap/core' +import { createCustomEditor, markdownThroughEditor, markdownThroughEditorHtml } from '../helpers' +import markdownit from '../../markdownit/index.js' + +describe('Preview extension', () => { + it('exposes toMarkdown function', () => { + const toMarkdown = getExtensionField(Preview, 'toMarkdown', Preview) + expect(typeof toMarkdown).toEqual('function') + }) + + it('exposes the toMarkdown function in the prosemirror schema', () => { + const editor = createCustomEditor({ + extensions: [Markdown, Preview] + }) + const preview = editor.schema.nodes.preview + expect(preview.spec.toMarkdown).toBeDefined() + }) + + it('markdown syntax is preserved through editor', () => { + const markdown = `[link](https://nextcloud.com (preview))` + expect(markdownThroughEditor(markdown)).toBe(markdown) + }) + + it('serializes HTML to markdown', () => { + const markdown = `[link](https://nextcloud.com (preview))` + const link = `link` + expect(markdownThroughEditorHtml(link)) + .toBe(markdown) + }) + + it('detects links', () => { + const link = `link` + const editor = createCustomEditor({ + extensions: [Markdown, Preview] + }) + editor.commands.setContent(`${link}

hello>

`) + const node = editor.state.doc.content.firstChild + expect(node.type.name).toBe('preview') + expect(node.attrs.title).toBe('preview') + expect(node.attrs.href).toBe('https://nextcloud.com') + }) + +}) From fe74b7bc66ad2bc11fcfd47adc6ab41c0e80246c Mon Sep 17 00:00:00 2001 From: Max Date: Tue, 20 Feb 2024 20:26:44 +0100 Subject: [PATCH 02/12] test(cy): make node test helpers available Signed-off-by: Max --- cypress/e2e/nodes/ListItem.spec.js | 44 +++-------------------- cypress/e2e/nodes/helpers.js | 58 ++++++++++++++++++++++++++++++ cypress/fixtures/Preview.md | 23 ++++++++++++ 3 files changed, 86 insertions(+), 39 deletions(-) create mode 100644 cypress/e2e/nodes/helpers.js create mode 100644 cypress/fixtures/Preview.md diff --git a/cypress/e2e/nodes/ListItem.spec.js b/cypress/e2e/nodes/ListItem.spec.js index 3af9a3760a8..62a98cb2ba6 100644 --- a/cypress/e2e/nodes/ListItem.spec.js +++ b/cypress/e2e/nodes/ListItem.spec.js @@ -5,11 +5,10 @@ import ListItem from '@tiptap/extension-list-item' import TaskList from './../../../src/nodes/TaskList.js' import TaskItem from './../../../src/nodes/TaskItem.js' import BulletList from './../../../src/nodes/BulletList.js' -import Markdown, { createMarkdownSerializer } from './../../../src/extensions/Markdown.js' -import { findChildren } from './../../../src/helpers/prosemirrorUtils.js' +import Markdown from './../../../src/extensions/Markdown.js' import { createCustomEditor } from './../../support/components.js' import testData from '../../fixtures/ListItem.md' -import markdownit from './../../../src/markdownit/index.js' +import { loadMarkdown, runCommands, expectMarkdown } from './helpers.js' describe('ListItem extension integrated in the editor', () => { @@ -37,42 +36,9 @@ describe('ListItem extension integrated in the editor', () => { expect(input).to.be.ok expect(output).to.be.ok /* eslint-enable no-unused-expressions */ - loadMarkdown(input) - runCommands() - expectMarkdown(output.replace(/\n*$/, '')) + loadMarkdown(editor, input) + runCommands(editor) + expectMarkdown(editor, output.replace(/\n*$/, '')) }) } - - const loadMarkdown = (markdown) => { - const stripped = markdown.replace(/\t*/g, '') - editor.commands.setContent(markdownit.render(stripped)) - } - - const runCommands = () => { - let found - while ((found = findCommand())) { - const { node, pos } = found - const name = node.text - editor.commands.setTextSelection(pos) - editor.commands[name]() - editor.commands.insertContent('did ') - } - } - - const findCommand = () => { - const doc = editor.state.doc - return findChildren(doc, child => { - return child.isText && Object.prototype.hasOwnProperty.call(editor.commands, child.text) - })[0] - } - - const expectMarkdown = (markdown) => { - const stripped = markdown.replace(/\t*/g, '') - expect(getMarkdown()).to.equal(stripped) - } - - const getMarkdown = () => { - const serializer = createMarkdownSerializer(editor.schema) - return serializer.serialize(editor.state.doc) - } }) diff --git a/cypress/e2e/nodes/helpers.js b/cypress/e2e/nodes/helpers.js new file mode 100644 index 00000000000..34770b79d20 --- /dev/null +++ b/cypress/e2e/nodes/helpers.js @@ -0,0 +1,58 @@ +/** + * @copyright Copyright (c) 2024 Max + * + * @author Max + * + * @license AGPL-3.0-or-later + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +import markdownit from './../../../src/markdownit/index.js' +import { findChildren } from './../../../src/helpers/prosemirrorUtils.js' +import { createMarkdownSerializer } from './../../../src/extensions/Markdown.js' + +export function loadMarkdown(editor, markdown) { + const stripped = markdown.replace(/\t*/g, '') + editor.commands.setContent(markdownit.render(stripped)) +} + +export function runCommands(editor) { + let found + while ((found = findCommand(editor))) { + const { node, pos } = found + const name = node.text + editor.commands.setTextSelection(pos) + editor.commands[name]() + editor.commands.insertContent('did ') + } +} + +function findCommand(editor){ + const doc = editor.state.doc + return findChildren(doc, child => { + return child.isText && Object.prototype.hasOwnProperty.call(editor.commands, child.text) + })[0] +} + +export function expectMarkdown(editor, markdown){ + const stripped = markdown.replace(/\t*/g, '') + expect(getMarkdown(editor)).to.equal(stripped) +} + +function getMarkdown(editor) { + const serializer = createMarkdownSerializer(editor.schema) + return serializer.serialize(editor.state.doc) +} diff --git a/cypress/fixtures/Preview.md b/cypress/fixtures/Preview.md new file mode 100644 index 00000000000..6e040e0bdeb --- /dev/null +++ b/cypress/fixtures/Preview.md @@ -0,0 +1,23 @@ +## Runs a spec + +empty + +--- + +empty + +## Preserves a link + +[link text](https://nextcloud.com) + +--- + +[link text](https://nextcloud.com) + +## Preserves a preview + +[link text](https://nextcloud.com (Preview)) + +--- + +[link text](https://nextcloud.com (Preview)) From 0d8c165ab8dc3b7a33c1c26d69ebeab21a45a7ad Mon Sep 17 00:00:00 2001 From: Max Date: Mon, 4 Mar 2024 15:01:29 +0100 Subject: [PATCH 03/12] enh(preview): togglePreview to return false when not possible Signed-off-by: Max --- cypress/e2e/nodes/Preview.spec.js | 134 ++++++++++++++++++++++++++++++ cypress/fixtures/Preview.md | 6 +- src/helpers/links.js | 2 +- src/nodes/Preview.js | 57 ++++++++++--- 4 files changed, 182 insertions(+), 17 deletions(-) create mode 100644 cypress/e2e/nodes/Preview.spec.js diff --git a/cypress/e2e/nodes/Preview.spec.js b/cypress/e2e/nodes/Preview.spec.js new file mode 100644 index 00000000000..ff1c29897c1 --- /dev/null +++ b/cypress/e2e/nodes/Preview.spec.js @@ -0,0 +1,134 @@ +/** + * @copyright Copyright (c) 2024 Max + * + * @author Max + * + * @license AGPL-3.0-or-later + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +import Markdown from './../../../src/extensions/Markdown.js' +import Preview from './../../../src/nodes/Preview.js' +import { Italic, Link } from './../../../src/marks/index.js' +import { createCustomEditor } from './../../support/components.js' +import testData from '../../fixtures/Preview.md' +import { loadMarkdown, runCommands, expectMarkdown } from './helpers.js' + +describe.only('Preview extension', { retries: 0 }, () => { + + const editor = createCustomEditor({ + content: '', + extensions: [ + Markdown, + Preview, + Link, + Italic, + ], + }) + + describe('togglePreview command', { retries: 0 }, () => { + + it('is available in commands', () => { + expect(editor.commands).to.have.property('togglePreview') + }) + + it('cannot run on normal paragraph', () => { + prepareEditor('hello\n') + expect(editor.can().togglePreview()).to.be.false + }) + + it('cannot run on a paragraph with a different mark', () => { + prepareEditor('*link text*\n') + expect(editor.can().togglePreview()).to.be.false + }) + + it('cannot run on a paragraph with a link without a href', () => { + prepareEditor('[link text]()\n') + expect(editor.can().togglePreview()).to.be.false + }) + + it('cannot run on a paragraph with an anchor link', () => { + prepareEditor('[link text](#top)\n') + expect(editor.can().togglePreview()).to.be.false + }) + + it('cannot run on a paragraph with other content', () => { + prepareEditor('[link text](https://nextcloud.com) hello\n') + expect(editor.can().togglePreview()).to.be.false + }) + + it('can run on a paragraph with a link', () => { + prepareEditor('[link text](https://nextcloud.com)\n') + expect(editor.can().togglePreview()).to.be.true + }) + + it('results in a preview node with the href', () => { + prepareEditor('[link text](https://nextcloud.com)\n') + editor.commands.togglePreview() + expect(getParentNode().type.name).to.equal('preview') + expect(getParentNode().attrs.href).to.equal('https://nextcloud.com') + }) + + it('reverts when toggled twice', () => { + prepareEditor('[link text](https://nextcloud.com)\n') + editor.commands.togglePreview() + editor.commands.togglePreview() + expect(getParentNode().type.name).to.equal('paragraph') + expect(getParentNode().attrs.href).to.equal('https://nextcloud.com') + }) + + }) + + function getParentNode() { + const { state: { selection } } = editor + return selection.$head.parent + } + + function prepareEditor(input) { + loadMarkdown(editor, input) + editor.commands.setTextSelection(1) + } + +}) + +describe('Markdown tests for Previews in the editor', { retries: 0 }, () => { + const editor = createCustomEditor({ + content: '', + extensions: [ + Markdown, + Preview, + Link, + ], + }) + + for (const spec of testData.split(/#+\s+/)) { + const [description, ...rest] = spec.split(/\n/) + const [input, output] = rest.join('\n').split(/\n\n---\n\n/) + if (!description) { + continue + } + it(description, () => { + expect(spec).to.include('\n') + /* eslint-disable no-unused-expressions */ + expect(input).to.be.ok + expect(output).to.be.ok + /* eslint-enable no-unused-expressions */ + loadMarkdown(editor, input) + runCommands(editor) + expectMarkdown(editor, output.replace(/\n*$/, '')) + }) + } +}) diff --git a/cypress/fixtures/Preview.md b/cypress/fixtures/Preview.md index 6e040e0bdeb..1b561d3b2b2 100644 --- a/cypress/fixtures/Preview.md +++ b/cypress/fixtures/Preview.md @@ -14,10 +14,10 @@ empty [link text](https://nextcloud.com) -## Preserves a preview +## Converts a link into a preview -[link text](https://nextcloud.com (Preview)) +[setPreviewENDLESS LOOP!](https://nextcloud.com (Preview)) --- -[link text](https://nextcloud.com (Preview)) +[did setPreview](https://nextcloud.com (Preview)) diff --git a/src/helpers/links.js b/src/helpers/links.js index 9470de76fe8..f3991c5d454 100644 --- a/src/helpers/links.js +++ b/src/helpers/links.js @@ -28,7 +28,7 @@ const domHref = function(node, relativePath) { if (!ref) { return ref } - if (!OCA.Viewer) { + if (!window.OCA?.Viewer) { return ref } if (ref.match(/^[a-zA-Z]*:/)) { diff --git a/src/nodes/Preview.js b/src/nodes/Preview.js index 2401633d418..89550b3cc58 100644 --- a/src/nodes/Preview.js +++ b/src/nodes/Preview.js @@ -1,9 +1,9 @@ -/* - * @copyright Copyright (c) 2022 Vinicius Reis +/** + * @copyright Copyright (c) 2024 Max * - * @author Vinicius Reis + * @author Max * - * @license GNU AGPL version 3 or any later version + * @license AGPL-3.0-or-later * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as @@ -20,7 +20,7 @@ * */ -import { Node, isNodeActive } from '@tiptap/core' +import { Node } from '@tiptap/core' import { domHref, parseHref } from './../helpers/links.js' import { VueNodeViewRenderer } from '@tiptap/vue-2' @@ -71,21 +71,52 @@ export default Node.create({ }, toMarkdown: (state, node) => { - state.write(`[`) + state.write('[') state.text(node.textContent, false) state.write(`](${node.attrs.href} (${node.attrs.title}))`) }, addCommands() { return { - setPreview: attrs => ({ commands }) => { - const attributes = { ...attrs, title: 'preview' } - return commands.setNode(this.name, attributes) - }, - togglePreview: attrs => ({ commands }) => { - const attributes = { ...attrs, title: 'preview' } - return commands.toggleNode(this.name, 'paragraph', attributes) + /** + * Turn a paragraph that contains a single link + * into a preview and vice versa. + * + */ + togglePreview: () => ({ state, commands }) => { + const { selection } = state + return previewPossible(selection) + && commands.toggleNode( + this.name, + 'paragraph', + previewAttributesFromSelection(selection), + ) }, } }, }) + +function previewAttributesFromSelection({ $from }) { + const href = extractHref($from.nodeAfter) + return { href, title: 'preview' } +} + +function previewPossible({ $from }) { + if (childCount($from.parent) > 1) { + return false + } + const href = extractHref($from.nodeAfter) + if (!href || href.startsWith('#')) { + return false + } + return true +} + +function extractHref(node) { + const link = node.marks.find(mark => mark.type.name === 'link') + return link?.attrs.href +} + +function childCount(node) { + return node.content.content.length +} From 9127692df710a7c6afc82d4c719badebd01692fa Mon Sep 17 00:00:00 2001 From: Max Date: Mon, 4 Mar 2024 16:39:10 +0100 Subject: [PATCH 04/12] enh(Preview): split togglePreview in setPreview and unsetPreview They actually need to do different things due to the different structure. On the one hand we have the preview node with plain text content. And on the other hand a paragraph node with the text wrapped in a link mark. Signed-off-by: Max --- cypress/e2e/nodes/Preview.spec.js | 63 ++++++++++++++++++++++++------- src/nodes/ParagraphView.vue | 2 +- src/nodes/Preview.js | 30 ++++++++++++--- src/nodes/Preview.vue | 4 +- 4 files changed, 79 insertions(+), 20 deletions(-) diff --git a/cypress/e2e/nodes/Preview.spec.js b/cypress/e2e/nodes/Preview.spec.js index ff1c29897c1..0271ad4ee06 100644 --- a/cypress/e2e/nodes/Preview.spec.js +++ b/cypress/e2e/nodes/Preview.spec.js @@ -39,55 +39,86 @@ describe.only('Preview extension', { retries: 0 }, () => { ], }) - describe('togglePreview command', { retries: 0 }, () => { + describe('setPreview command', { retries: 0 }, () => { it('is available in commands', () => { - expect(editor.commands).to.have.property('togglePreview') + expect(editor.commands).to.have.property('setPreview') }) it('cannot run on normal paragraph', () => { prepareEditor('hello\n') - expect(editor.can().togglePreview()).to.be.false + expect(editor.can().setPreview()).to.be.false }) it('cannot run on a paragraph with a different mark', () => { prepareEditor('*link text*\n') - expect(editor.can().togglePreview()).to.be.false + expect(editor.can().setPreview()).to.be.false }) it('cannot run on a paragraph with a link without a href', () => { prepareEditor('[link text]()\n') - expect(editor.can().togglePreview()).to.be.false + expect(editor.can().setPreview()).to.be.false }) it('cannot run on a paragraph with an anchor link', () => { prepareEditor('[link text](#top)\n') - expect(editor.can().togglePreview()).to.be.false + expect(editor.can().setPreview()).to.be.false }) it('cannot run on a paragraph with other content', () => { prepareEditor('[link text](https://nextcloud.com) hello\n') - expect(editor.can().togglePreview()).to.be.false + expect(editor.can().setPreview()).to.be.false }) it('can run on a paragraph with a link', () => { prepareEditor('[link text](https://nextcloud.com)\n') - expect(editor.can().togglePreview()).to.be.true + expect(editor.can().setPreview()).to.be.true }) it('results in a preview node with the href', () => { prepareEditor('[link text](https://nextcloud.com)\n') - editor.commands.togglePreview() + editor.commands.setPreview() expect(getParentNode().type.name).to.equal('preview') expect(getParentNode().attrs.href).to.equal('https://nextcloud.com') }) - it('reverts when toggled twice', () => { + it('cannot run twice', () => { prepareEditor('[link text](https://nextcloud.com)\n') - editor.commands.togglePreview() - editor.commands.togglePreview() + editor.commands.setPreview() + expect(editor.can().setPreview()).to.be.false + }) + + }) + + describe('unsetPreview command', { retries: 0 }, () => { + + it('is available in commands', () => { + expect(editor.commands).to.have.property('unsetPreview') + }) + + it('cannot run on normal paragraph', () => { + prepareEditor('hello\n') + expect(editor.can().unsetPreview()).to.be.false + }) + + it('can run on the output of setPreview', () => { + prepareEditor('[link text](https://nextcloud.com)\n') + editor.commands.setPreview() + expect(editor.can().unsetPreview()).to.be.true + }) + + it('creates a paragraph', () => { + prepareEditor('[link text](https://nextcloud.com)\n') + editor.commands.setPreview() + editor.commands.unsetPreview() expect(getParentNode().type.name).to.equal('paragraph') - expect(getParentNode().attrs.href).to.equal('https://nextcloud.com') + }) + + it('includes a link', () => { + prepareEditor('[link text](https://nextcloud.com)\n') + editor.commands.setPreview() + editor.commands.unsetPreview() + expect(getMark().attrs.href).to.equal('https://nextcloud.com') }) }) @@ -97,6 +128,12 @@ describe.only('Preview extension', { retries: 0 }, () => { return selection.$head.parent } + function getMark() { + const { state: { selection } } = editor + console.info(selection.$head) + return selection.$head.nodeAfter.marks[0] + } + function prepareEditor(input) { loadMarkdown(editor, input) editor.commands.setTextSelection(1) diff --git a/src/nodes/ParagraphView.vue b/src/nodes/ParagraphView.vue index 04d32b3d3b2..9a5f25de329 100644 --- a/src/nodes/ParagraphView.vue +++ b/src/nodes/ParagraphView.vue @@ -80,7 +80,7 @@ export default { this.$editor.chain() .focus() .setTextSelection(this.getPos()) - .togglePreview({ href: this.href }) + .setPreview({ href: this.href }) .run() }, getTextReference(node) { diff --git a/src/nodes/Preview.js b/src/nodes/Preview.js index 89550b3cc58..d6ca1677c16 100644 --- a/src/nodes/Preview.js +++ b/src/nodes/Preview.js @@ -20,7 +20,7 @@ * */ -import { Node } from '@tiptap/core' +import { Node, isNodeActive, getNodeType } from '@tiptap/core' import { domHref, parseHref } from './../helpers/links.js' import { VueNodeViewRenderer } from '@tiptap/vue-2' @@ -78,20 +78,35 @@ export default Node.create({ addCommands() { return { + /** * Turn a paragraph that contains a single link - * into a preview and vice versa. + * into a preview. * */ - togglePreview: () => ({ state, commands }) => { + setPreview: () => ({ state, commands }) => { const { selection } = state return previewPossible(selection) - && commands.toggleNode( + && commands.setNode( this.name, - 'paragraph', previewAttributesFromSelection(selection), ) }, + + /** + * Turn a preview back into a paragraph + * that contains a single link. + * + */ + unsetPreview: () => ({ state, chain }) => { + const { selection } = state + return isPreview(this.name, this.attributes, state) + && chain() + .setNode('paragraph') + .setLink(this.attributes) + .run() + }, + } }, }) @@ -101,6 +116,11 @@ function previewAttributesFromSelection({ $from }) { return { href, title: 'preview' } } +function isPreview(typeOrName, attributes, state) { + const type = getNodeType(typeOrName, state.schema) + return isNodeActive(state, type, attributes) +} + function previewPossible({ $from }) { if (childCount($from.parent) > 1) { return false diff --git a/src/nodes/Preview.vue b/src/nodes/Preview.vue index 1c90d69a8e0..efc3df6be67 100644 --- a/src/nodes/Preview.vue +++ b/src/nodes/Preview.vue @@ -37,6 +37,7 @@ import { nodeViewProps, NodeViewWrapper, NodeViewContent } from '@tiptap/vue-2' import { NcReferenceList } from '@nextcloud/vue/dist/Components/NcRichText.js' import PreviewOptions from '../components/Editor/PreviewOptions.vue' +import { useEditorMixin } from '../components/Editor.provider.js' export default { name: 'Preview', @@ -52,13 +53,14 @@ export default { value: 'link-preview', } }, + mixins: [ useEditorMixin ], methods: { convertToParagraph(...args) { console.info(...args) this.$editor.chain() .focus() .setTextSelection(this.getPos()) - .setNode('paragaph') + .unsetPreview() .run() }, }, From 1fc7bb76d427722dfddc5217cd30f53e1c925411 Mon Sep 17 00:00:00 2001 From: Max Date: Tue, 5 Mar 2024 10:31:33 +0100 Subject: [PATCH 05/12] test(preview): adjust jest tests Signed-off-by: Max --- src/tests/markdown.spec.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/tests/markdown.spec.js b/src/tests/markdown.spec.js index 008cb6bae3b..7581ea66ee6 100644 --- a/src/tests/markdown.spec.js +++ b/src/tests/markdown.spec.js @@ -104,12 +104,12 @@ describe('Markdown though editor', () => { }) test('preview with url only', () => { - const entry = '::: preview\nhttps://nextcloud.com\n\n:::' + const entry = '[https://nextcloud.com](https://nextcloud.com (preview))' expect(markdownThroughEditor(entry)).toBe(entry) }) test('preview with text', () => { - const entry = '::: preview\n[text](https://nextcloud.com)\n\n:::' + const entry = '[some other text](https://nextcloud.com (preview))' expect(markdownThroughEditor(entry)).toBe(entry) }) From 5453b33882ad05392b40316152dd49b5eb464b4d Mon Sep 17 00:00:00 2001 From: Max Date: Tue, 5 Mar 2024 10:47:25 +0100 Subject: [PATCH 06/12] refactor(preview): use selection in the function args Signed-off-by: Max --- src/nodes/Preview.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/nodes/Preview.js b/src/nodes/Preview.js index d6ca1677c16..6385e3b3d32 100644 --- a/src/nodes/Preview.js +++ b/src/nodes/Preview.js @@ -85,11 +85,10 @@ export default Node.create({ * */ setPreview: () => ({ state, commands }) => { - const { selection } = state - return previewPossible(selection) + return previewPossible(state) && commands.setNode( this.name, - previewAttributesFromSelection(selection), + previewAttributesFromSelection(state), ) }, @@ -99,7 +98,6 @@ export default Node.create({ * */ unsetPreview: () => ({ state, chain }) => { - const { selection } = state return isPreview(this.name, this.attributes, state) && chain() .setNode('paragraph') @@ -111,7 +109,8 @@ export default Node.create({ }, }) -function previewAttributesFromSelection({ $from }) { +function previewAttributesFromSelection({ selection }) { + const { $from } = selection const href = extractHref($from.nodeAfter) return { href, title: 'preview' } } @@ -121,7 +120,8 @@ function isPreview(typeOrName, attributes, state) { return isNodeActive(state, type, attributes) } -function previewPossible({ $from }) { +function previewPossible({ selection }) { + const { $from } = selection if (childCount($from.parent) > 1) { return false } From eac2bf183bdfaa86d07d3526d62142b2db0db924 Mon Sep 17 00:00:00 2001 From: Max Date: Wed, 6 Mar 2024 16:07:55 +0100 Subject: [PATCH 07/12] enh(preview): make use of setPreview without args Signed-off-by: Max --- src/nodes/ParagraphView.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/nodes/ParagraphView.vue b/src/nodes/ParagraphView.vue index 9a5f25de329..653d5c0b498 100644 --- a/src/nodes/ParagraphView.vue +++ b/src/nodes/ParagraphView.vue @@ -80,7 +80,7 @@ export default { this.$editor.chain() .focus() .setTextSelection(this.getPos()) - .setPreview({ href: this.href }) + .setPreview() .run() }, getTextReference(node) { From 75f5479f04ff3b60edfacf822babb67ea0e2e309 Mon Sep 17 00:00:00 2001 From: Max Date: Wed, 6 Mar 2024 17:48:22 +0100 Subject: [PATCH 08/12] fix: get rid of link in preview to avoid popup Signed-off-by: Max --- cypress/e2e/nodes/Preview.spec.js | 3 ++- src/nodes/Preview.js | 11 ++++++----- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/cypress/e2e/nodes/Preview.spec.js b/cypress/e2e/nodes/Preview.spec.js index 0271ad4ee06..4ca3101ae78 100644 --- a/cypress/e2e/nodes/Preview.spec.js +++ b/cypress/e2e/nodes/Preview.spec.js @@ -75,11 +75,12 @@ describe.only('Preview extension', { retries: 0 }, () => { expect(editor.can().setPreview()).to.be.true }) - it('results in a preview node with the href', () => { + it('results in a preview node with the href and plain text', () => { prepareEditor('[link text](https://nextcloud.com)\n') editor.commands.setPreview() expect(getParentNode().type.name).to.equal('preview') expect(getParentNode().attrs.href).to.equal('https://nextcloud.com') + expect(getMark()).to.be.undefined }) it('cannot run twice', () => { diff --git a/src/nodes/Preview.js b/src/nodes/Preview.js index 6385e3b3d32..e330f7d1922 100644 --- a/src/nodes/Preview.js +++ b/src/nodes/Preview.js @@ -84,12 +84,12 @@ export default Node.create({ * into a preview. * */ - setPreview: () => ({ state, commands }) => { + setPreview: () => ({ state, chain }) => { return previewPossible(state) - && commands.setNode( - this.name, - previewAttributesFromSelection(state), - ) + && chain() + .unsetLink() + .setNode(this.name, previewAttributesFromSelection(state)) + .run() }, /** @@ -98,6 +98,7 @@ export default Node.create({ * */ unsetPreview: () => ({ state, chain }) => { + console.info(this.attributes) return isPreview(this.name, this.attributes, state) && chain() .setNode('paragraph') From 1ee3cbdf7496c8340159b1854fd2090146ae0d76 Mon Sep 17 00:00:00 2001 From: Max Date: Wed, 6 Mar 2024 17:57:56 +0100 Subject: [PATCH 09/12] fix(preview): hide tooltip for links in preview Signed-off-by: Max --- cypress/e2e/nodes/Preview.spec.js | 4 ++-- src/extensions/LinkBubblePluginView.js | 6 ++++++ src/nodes/Preview.js | 2 -- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/cypress/e2e/nodes/Preview.spec.js b/cypress/e2e/nodes/Preview.spec.js index 4ca3101ae78..c701f635c42 100644 --- a/cypress/e2e/nodes/Preview.spec.js +++ b/cypress/e2e/nodes/Preview.spec.js @@ -75,12 +75,12 @@ describe.only('Preview extension', { retries: 0 }, () => { expect(editor.can().setPreview()).to.be.true }) - it('results in a preview node with the href and plain text', () => { + it('results in a preview node with the href and text with link mark', () => { prepareEditor('[link text](https://nextcloud.com)\n') editor.commands.setPreview() expect(getParentNode().type.name).to.equal('preview') expect(getParentNode().attrs.href).to.equal('https://nextcloud.com') - expect(getMark()).to.be.undefined + expect(getMark().attrs.href).to.equal('https://nextcloud.com') }) it('cannot run twice', () => { diff --git a/src/extensions/LinkBubblePluginView.js b/src/extensions/LinkBubblePluginView.js index 6e968713253..3f5d09f53dd 100644 --- a/src/extensions/LinkBubblePluginView.js +++ b/src/extensions/LinkBubblePluginView.js @@ -195,6 +195,12 @@ class LinkBubblePluginView { const to = Math.max(...ranges.map(range => range.$to.pos)) const resolved = view.state.doc.resolve(from) + + // ignore links in previews + if (resolved.parent.type.name === 'preview') { + return false + } + const node = resolved.parent.maybeChild(resolved.index()) const nodeStart = resolved.pos - resolved.textOffset const nodeEnd = nodeStart + node?.nodeSize diff --git a/src/nodes/Preview.js b/src/nodes/Preview.js index e330f7d1922..a11e69dea38 100644 --- a/src/nodes/Preview.js +++ b/src/nodes/Preview.js @@ -87,7 +87,6 @@ export default Node.create({ setPreview: () => ({ state, chain }) => { return previewPossible(state) && chain() - .unsetLink() .setNode(this.name, previewAttributesFromSelection(state)) .run() }, @@ -102,7 +101,6 @@ export default Node.create({ return isPreview(this.name, this.attributes, state) && chain() .setNode('paragraph') - .setLink(this.attributes) .run() }, From 544bfb6dacb7d312e6e821e4fb626896fff8ea23 Mon Sep 17 00:00:00 2001 From: Max Date: Wed, 6 Mar 2024 18:50:57 +0100 Subject: [PATCH 10/12] fix(preview): set selection inside the node Signed-off-by: Max --- cypress/e2e/nodes/Preview.spec.js | 6 ++++++ src/nodes/ParagraphView.vue | 2 +- src/nodes/Preview.vue | 2 +- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/cypress/e2e/nodes/Preview.spec.js b/cypress/e2e/nodes/Preview.spec.js index c701f635c42..02135a66cd7 100644 --- a/cypress/e2e/nodes/Preview.spec.js +++ b/cypress/e2e/nodes/Preview.spec.js @@ -75,6 +75,12 @@ describe.only('Preview extension', { retries: 0 }, () => { expect(editor.can().setPreview()).to.be.true }) + it('can run the second a paragraph with a link', () => { + prepareEditor('hello\n\n[link text](https://nextcloud.com)\n') + editor.commands.setTextSelection(10) + expect(editor.can().setPreview()).to.be.true + }) + it('results in a preview node with the href and text with link mark', () => { prepareEditor('[link text](https://nextcloud.com)\n') editor.commands.setPreview() diff --git a/src/nodes/ParagraphView.vue b/src/nodes/ParagraphView.vue index 653d5c0b498..6db8c6482bb 100644 --- a/src/nodes/ParagraphView.vue +++ b/src/nodes/ParagraphView.vue @@ -79,7 +79,7 @@ export default { console.info(...args) this.$editor.chain() .focus() - .setTextSelection(this.getPos()) + .setTextSelection(this.getPos() + 1) .setPreview() .run() }, diff --git a/src/nodes/Preview.vue b/src/nodes/Preview.vue index efc3df6be67..b3abed0ef79 100644 --- a/src/nodes/Preview.vue +++ b/src/nodes/Preview.vue @@ -59,7 +59,7 @@ export default { console.info(...args) this.$editor.chain() .focus() - .setTextSelection(this.getPos()) + .setTextSelection(this.getPos() + 1) .unsetPreview() .run() }, From dd12e5aa60b83318343c6a03a126c14e83177df7 Mon Sep 17 00:00:00 2001 From: Max Date: Wed, 6 Mar 2024 19:08:35 +0100 Subject: [PATCH 11/12] test(preview): do not run commands in Preview md test They convert the node without changing the content resulting in an endless loop. Signed-off-by: Max --- cypress/fixtures/Preview.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cypress/fixtures/Preview.md b/cypress/fixtures/Preview.md index 1b561d3b2b2..f9125046043 100644 --- a/cypress/fixtures/Preview.md +++ b/cypress/fixtures/Preview.md @@ -14,10 +14,10 @@ empty [link text](https://nextcloud.com) -## Converts a link into a preview +## Preserves a link preview -[setPreviewENDLESS LOOP!](https://nextcloud.com (Preview)) +[link text](https://nextcloud.com (Preview)) --- -[did setPreview](https://nextcloud.com (Preview)) +[link text](https://nextcloud.com (Preview)) From fecb336ebc030ddccd9c05f8e46340a73059a350 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julius=20H=C3=A4rtl?= Date: Thu, 7 Mar 2024 07:50:32 +0100 Subject: [PATCH 12/12] style: Fix eslint errors MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Julius Härtl --- cypress/e2e/nodes/Preview.spec.js | 11 ++++++++++ cypress/e2e/nodes/helpers.js | 26 ++++++++++++++++++++++-- src/components/Editor/PreviewOptions.vue | 12 +++++------ src/markdownit/preview.js | 23 +++++++++++++++++---- src/nodes/ParagraphView.vue | 2 +- src/nodes/Preview.js | 24 ++++++++++++++++++++++ src/nodes/Preview.vue | 2 +- 7 files changed, 86 insertions(+), 14 deletions(-) diff --git a/cypress/e2e/nodes/Preview.spec.js b/cypress/e2e/nodes/Preview.spec.js index 02135a66cd7..ab90ca0769a 100644 --- a/cypress/e2e/nodes/Preview.spec.js +++ b/cypress/e2e/nodes/Preview.spec.js @@ -1,3 +1,4 @@ +/* eslint-disable no-unused-expressions */ /** * @copyright Copyright (c) 2024 Max * @@ -130,17 +131,27 @@ describe.only('Preview extension', { retries: 0 }, () => { }) + /** + * + */ function getParentNode() { const { state: { selection } } = editor return selection.$head.parent } + /** + * + */ function getMark() { const { state: { selection } } = editor console.info(selection.$head) return selection.$head.nodeAfter.marks[0] } + /** + * + * @param input + */ function prepareEditor(input) { loadMarkdown(editor, input) editor.commands.setTextSelection(1) diff --git a/cypress/e2e/nodes/helpers.js b/cypress/e2e/nodes/helpers.js index 34770b79d20..4886dd8243a 100644 --- a/cypress/e2e/nodes/helpers.js +++ b/cypress/e2e/nodes/helpers.js @@ -24,11 +24,20 @@ import markdownit from './../../../src/markdownit/index.js' import { findChildren } from './../../../src/helpers/prosemirrorUtils.js' import { createMarkdownSerializer } from './../../../src/extensions/Markdown.js' +/** + * + * @param editor + * @param markdown + */ export function loadMarkdown(editor, markdown) { const stripped = markdown.replace(/\t*/g, '') editor.commands.setContent(markdownit.render(stripped)) } +/** + * + * @param editor + */ export function runCommands(editor) { let found while ((found = findCommand(editor))) { @@ -40,18 +49,31 @@ export function runCommands(editor) { } } -function findCommand(editor){ +/** + * + * @param editor + */ +function findCommand(editor) { const doc = editor.state.doc return findChildren(doc, child => { return child.isText && Object.prototype.hasOwnProperty.call(editor.commands, child.text) })[0] } -export function expectMarkdown(editor, markdown){ +/** + * + * @param editor + * @param markdown + */ +export function expectMarkdown(editor, markdown) { const stripped = markdown.replace(/\t*/g, '') expect(getMarkdown(editor)).to.equal(stripped) } +/** + * + * @param editor + */ function getMarkdown(editor) { const serializer = createMarkdownSerializer(editor.schema) return serializer.serialize(editor.state.doc) diff --git a/src/components/Editor/PreviewOptions.vue b/src/components/Editor/PreviewOptions.vue index 8e19892d2eb..7242b4f3ba0 100644 --- a/src/components/Editor/PreviewOptions.vue +++ b/src/components/Editor/PreviewOptions.vue @@ -50,18 +50,18 @@ import DotsVerticalIcon from 'vue-material-design-icons/DotsVertical.vue' export default { name: 'PreviewOptions', - props: { - value: { - type: String, - required: true, - }, - }, components: { DotsVerticalIcon, NcActions, NcActionCaption, NcActionRadio, }, + props: { + value: { + type: String, + required: true, + }, + }, } diff --git a/src/markdownit/preview.js b/src/markdownit/preview.js index 559839b3751..b403f7aca29 100644 --- a/src/markdownit/preview.js +++ b/src/markdownit/preview.js @@ -20,8 +20,13 @@ * */ +/** + * + * @param tokens + * @param i + */ function isPreviewLinkInParagraph(tokens, i) { - const [prev, cur, next] = tokens.slice(i-1, i+2) + const [prev, cur, next] = tokens.slice(i - 1, i + 2) return prev.type === 'paragraph_open' && cur.type === 'inline' && cur.children @@ -38,10 +43,15 @@ function isPreviewLinkInParagraph(tokens, i) { * @param {array} tokens - the token stream to modify * @param {Number} i - index of the token to unwrap */ +/** + * + * @param tokens + * @param i + */ function unwrapToken(tokens, i) { // Start from the end so indexes stay the same. - tokens.splice(i+1, 1) - tokens.splice(i-1, 1) + tokens.splice(i + 1, 1) + tokens.splice(i - 1, 1) } /** @@ -49,6 +59,11 @@ function unwrapToken(tokens, i) { */ export default (md) => { + /** + * + * @param root0 + * @param root0.tokens + */ function linkPreviews({ tokens }) { // do not process first and last token for (let i = 1, l = tokens.length; i < (l - 1); ++i) { @@ -58,5 +73,5 @@ export default (md) => { } } - md.core.ruler.before('linkify', 'link_previews', linkPreviews); + md.core.ruler.before('linkify', 'link_previews', linkPreviews) } diff --git a/src/nodes/ParagraphView.vue b/src/nodes/ParagraphView.vue index 6db8c6482bb..9d77e980982 100644 --- a/src/nodes/ParagraphView.vue +++ b/src/nodes/ParagraphView.vue @@ -43,6 +43,7 @@ export default { NodeViewContent, PreviewOptions, }, + mixins: [useEditorMixin], props: nodeViewProps, data() { return { @@ -51,7 +52,6 @@ export default { value: 'text-only', } }, - mixins: [ useEditorMixin ], watch: { node: { handler(newNode) { diff --git a/src/nodes/Preview.js b/src/nodes/Preview.js index a11e69dea38..94628bda3a0 100644 --- a/src/nodes/Preview.js +++ b/src/nodes/Preview.js @@ -108,17 +108,33 @@ export default Node.create({ }, }) +/** + * + * @param root0 + * @param root0.selection + */ function previewAttributesFromSelection({ selection }) { const { $from } = selection const href = extractHref($from.nodeAfter) return { href, title: 'preview' } } +/** + * + * @param typeOrName + * @param attributes + * @param state + */ function isPreview(typeOrName, attributes, state) { const type = getNodeType(typeOrName, state.schema) return isNodeActive(state, type, attributes) } +/** + * + * @param root0 + * @param root0.selection + */ function previewPossible({ selection }) { const { $from } = selection if (childCount($from.parent) > 1) { @@ -131,11 +147,19 @@ function previewPossible({ selection }) { return true } +/** + * + * @param node + */ function extractHref(node) { const link = node.marks.find(mark => mark.type.name === 'link') return link?.attrs.href } +/** + * + * @param node + */ function childCount(node) { return node.content.content.length } diff --git a/src/nodes/Preview.vue b/src/nodes/Preview.vue index b3abed0ef79..8375fe16341 100644 --- a/src/nodes/Preview.vue +++ b/src/nodes/Preview.vue @@ -47,13 +47,13 @@ export default { NcReferenceList, PreviewOptions, }, + mixins: [useEditorMixin], props: nodeViewProps, data() { return { value: 'link-preview', } }, - mixins: [ useEditorMixin ], methods: { convertToParagraph(...args) { console.info(...args)