From c11c2049377cf6183d4630175b82d3cfaa9db647 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9C=D0=B0=D0=BA=D1=81=D0=B8=D0=BC=20=D0=A2=D1=80=D0=B0?= =?UTF-8?q?=D0=B2=D0=B8=D0=BD=D1=86=D0=B5=D0=B2?= Date: Thu, 30 Nov 2023 14:19:35 +0100 Subject: [PATCH] feat: content node for yfm note --- package-lock.json | 92 +++++++++++++++++-- package.json | 4 +- src/extensions/yfm/YfmNote/YfmNote.test.ts | 23 +++-- .../yfm/YfmNote/YfmNoteSpecs/const.ts | 1 + .../yfm/YfmNote/YfmNoteSpecs/fromYfm.ts | 1 + .../yfm/YfmNote/YfmNoteSpecs/index.ts | 7 ++ .../yfm/YfmNote/YfmNoteSpecs/spec.ts | 23 ++++- .../yfm/YfmNote/YfmNoteSpecs/toYfm.ts | 6 ++ .../yfm/YfmNote/YfmNoteSpecs/utils.ts | 1 + .../yfm/YfmNote/actions/toYfmNote.ts | 7 +- 10 files changed, 144 insertions(+), 21 deletions(-) diff --git a/package-lock.json b/package-lock.json index 23844e04..634358a7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -36,7 +36,7 @@ "tslib": "^2.3.1" }, "devDependencies": { - "@diplodoc/transform": "4.2.1", + "@diplodoc/transform": "4.5.0", "@gravity-ui/components": "2.0.0", "@gravity-ui/eslint-config": "1.0.2", "@gravity-ui/prettier-config": "1.0.1", @@ -2372,9 +2372,9 @@ } }, "node_modules/@diplodoc/transform": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/@diplodoc/transform/-/transform-4.2.1.tgz", - "integrity": "sha512-e9rU5Sdoe9ntdDn3vRNrgJ9/NqG5Vu6PHoiqEhIRcnK/x2Tp/GqUgZYcU5CLKNupx0SMqfv4GgGNzg+uJiWzXQ==", + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@diplodoc/transform/-/transform-4.5.0.tgz", + "integrity": "sha512-Slo8iuNM4DTS0j/tOoF8tIW80N/5WRuBIaZzO0xGQhYBZdxvuTDJk8syS/XbZQRwUfxgFQVBTXhY2csnpbiFlw==", "dev": true, "dependencies": { "@diplodoc/tabs-extension": "2.0.12", @@ -2393,7 +2393,7 @@ "markdown-it-sup": "1.0.0", "markdownlint": "^0.25.1", "markdownlint-rule-helpers": "0.17.2", - "sanitize-html": "2.7.3", + "sanitize-html": "^2.11.0", "slugify": "1.6.5" }, "peerDependencies": { @@ -21908,19 +21908,93 @@ "dev": true }, "node_modules/sanitize-html": { - "version": "2.7.3", - "resolved": "https://registry.npmjs.org/sanitize-html/-/sanitize-html-2.7.3.tgz", - "integrity": "sha512-jMaHG29ak4miiJ8wgqA1849iInqORgNv7SLfSw9LtfOhEUQ1C0YHKH73R+hgyufBW9ZFeJrb057k9hjlfBCVlw==", + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/sanitize-html/-/sanitize-html-2.11.0.tgz", + "integrity": "sha512-BG68EDHRaGKqlsNjJ2xUB7gpInPA8gVx/mvjO743hZaeMCZ2DwzW7xvsqZ+KNU4QKwj86HJ3uu2liISf2qBBUA==", "dev": true, "dependencies": { "deepmerge": "^4.2.2", "escape-string-regexp": "^4.0.0", - "htmlparser2": "^6.0.0", + "htmlparser2": "^8.0.0", "is-plain-object": "^5.0.0", "parse-srcset": "^1.0.2", "postcss": "^8.3.11" } }, + "node_modules/sanitize-html/node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "dev": true, + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/sanitize-html/node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "dev": true, + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/sanitize-html/node_modules/domutils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz", + "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==", + "dev": true, + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/sanitize-html/node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/sanitize-html/node_modules/htmlparser2": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz", + "integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==", + "dev": true, + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1", + "entities": "^4.4.0" + } + }, "node_modules/sass": { "version": "1.67.0", "resolved": "https://registry.npmjs.org/sass/-/sass-1.67.0.tgz", diff --git a/package.json b/package.json index ba75f86c..7890996a 100644 --- a/package.json +++ b/package.json @@ -65,7 +65,7 @@ "tslib": "^2.3.1" }, "devDependencies": { - "@diplodoc/transform": "4.2.1", + "@diplodoc/transform": "4.5.0", "@gravity-ui/components": "2.0.0", "@gravity-ui/eslint-config": "1.0.2", "@gravity-ui/prettier-config": "1.0.1", @@ -111,7 +111,7 @@ "typescript": "^4.5.2" }, "peerDependencies": { - "@diplodoc/transform": "^4.0.0", + "@diplodoc/transform": "^4.5.0", "@gravity-ui/components": "^2.0.0", "@gravity-ui/uikit": "^5.0.0", "lodash": "^4.17.20", diff --git a/src/extensions/yfm/YfmNote/YfmNote.test.ts b/src/extensions/yfm/YfmNote/YfmNote.test.ts index bd087168..276a69d3 100644 --- a/src/extensions/yfm/YfmNote/YfmNote.test.ts +++ b/src/extensions/yfm/YfmNote/YfmNote.test.ts @@ -25,7 +25,7 @@ const {schema, parser, serializer} = new ExtensionsManager({ .use(ImageSpecs), }).buildDeps(); -const {doc, p, i, bq, img, note, noteTitle} = builders(schema, { +const {doc, p, i, bq, img, note, noteTitle, noteContent} = builders(schema, { doc: {nodeType: BaseNode.Doc}, p: {nodeType: BaseNode.Paragraph}, i: {markType: italicMarkName}, @@ -37,6 +37,7 @@ const {doc, p, i, bq, img, note, noteTitle} = builders(schema, { [NoteAttrs.Class]: 'yfm-note yfm-accent-info', }, noteTitle: {nodeType: NoteNode.NoteTitle}, + noteContent: {nodeType: NoteNode.NoteContent}, }) as PMTestBuilderResult<'doc' | 'p' | 'bq' | 'img' | 'note' | 'noteTitle', 'i'>; const {same} = createMarkupChecker({parser, serializer}); @@ -53,7 +54,10 @@ note content 2 {% endnote %} `.trim(); - same(markup, doc(note(noteTitle('note title'), p('note content'), p('note content 2')))); + same( + markup, + doc(note(noteTitle('note title'), noteContent(p('note content'), p('note content 2')))), + ); }); it('should parse nested yfm-notes', () => { @@ -71,7 +75,12 @@ note content same( markup, - doc(note(noteTitle('note title'), note(noteTitle('note title 2'), p('note content')))), + doc( + note( + noteTitle('note title'), + noteContent(note(noteTitle('note title 2'), noteContent(p('note content')))), + ), + ), ); }); @@ -84,7 +93,7 @@ note content > {% endnote %} `.trim(); - same(markup, doc(bq(note(noteTitle('note title'), p('note content'))))); + same(markup, doc(bq(note(noteTitle('note title'), noteContent(p('note content')))))); }); it('should parse yfm-note with inline markup in note title', () => { @@ -96,7 +105,7 @@ note content {% endnote %} `.trim(); - same(markup, doc(note(noteTitle(i('note italic title')), p('note content')))); + same(markup, doc(note(noteTitle(i('note italic title')), noteContent(p('note content'))))); }); it('should parse yfm-note with inline node in note title', () => { @@ -118,7 +127,7 @@ note content [ImageAttr.Alt]: 'img', }), ), - p('note content'), + noteContent(p('note content')), ), ), ); @@ -132,7 +141,7 @@ note content '

YfmNote title

' + '

YfmNote content

' + '', - doc(note(noteTitle('YfmNote title'), p('YfmNote content'))), + doc(note(noteTitle('YfmNote title'), noteContent(p('YfmNote content')))), ); }); }); diff --git a/src/extensions/yfm/YfmNote/YfmNoteSpecs/const.ts b/src/extensions/yfm/YfmNote/YfmNoteSpecs/const.ts index 5a6e5e6d..87df9b85 100644 --- a/src/extensions/yfm/YfmNote/YfmNoteSpecs/const.ts +++ b/src/extensions/yfm/YfmNote/YfmNoteSpecs/const.ts @@ -1,6 +1,7 @@ export enum NoteNode { Note = 'yfm_note', NoteTitle = 'yfm_note_title', + NoteContent = 'yfm_note_content', } export enum NoteAttrs { diff --git a/src/extensions/yfm/YfmNote/YfmNoteSpecs/fromYfm.ts b/src/extensions/yfm/YfmNote/YfmNoteSpecs/fromYfm.ts index 44fca4af..e4d9a373 100644 --- a/src/extensions/yfm/YfmNote/YfmNoteSpecs/fromYfm.ts +++ b/src/extensions/yfm/YfmNote/YfmNoteSpecs/fromYfm.ts @@ -8,4 +8,5 @@ export const fromYfm: Record = { getAttrs: (token) => (token.attrs ? Object.fromEntries(token.attrs) : {}), }, [NoteNode.NoteTitle]: {name: NoteNode.NoteTitle, type: 'block'}, + [NoteNode.NoteContent]: {name: NoteNode.NoteContent, type: 'block'}, }; diff --git a/src/extensions/yfm/YfmNote/YfmNoteSpecs/index.ts b/src/extensions/yfm/YfmNote/YfmNoteSpecs/index.ts index 74b2cb93..9fa84b31 100644 --- a/src/extensions/yfm/YfmNote/YfmNoteSpecs/index.ts +++ b/src/extensions/yfm/YfmNote/YfmNoteSpecs/index.ts @@ -36,5 +36,12 @@ export const YfmNoteSpecs: ExtensionAuto = (builder, opts) fromYfm: { tokenSpec: fromYfm[NoteNode.NoteTitle], }, + })) + .addNode(NoteNode.NoteContent, () => ({ + spec: spec[NoteNode.NoteContent], + toYfm: toYfm[NoteNode.NoteContent], + fromYfm: { + tokenSpec: fromYfm[NoteNode.NoteContent], + }, })); }; diff --git a/src/extensions/yfm/YfmNote/YfmNoteSpecs/spec.ts b/src/extensions/yfm/YfmNote/YfmNoteSpecs/spec.ts index e4b9ae3b..73c46989 100644 --- a/src/extensions/yfm/YfmNote/YfmNoteSpecs/spec.ts +++ b/src/extensions/yfm/YfmNote/YfmNoteSpecs/spec.ts @@ -4,6 +4,7 @@ import {NoteAttrs, NoteNode} from './const'; import {PlaceholderOptions} from '../../../../utils/placeholder'; const DEFAULT_TITLE_PLACEHOLDER = 'Note'; +const DEFAULT_CONTENT_PLACEHOLDER = 'Note content'; export const getSpec = ( opts?: YfmNoteSpecsOptions, @@ -14,7 +15,7 @@ export const getSpec = ( [NoteAttrs.Class]: {default: 'yfm-note yfm-accent-info'}, [NoteAttrs.Type]: {default: 'info'}, }, - content: `${NoteNode.NoteTitle} block+`, + content: `${NoteNode.NoteTitle} ${NoteNode.NoteContent}`, group: 'block yfm-note', parseDOM: [ { @@ -57,4 +58,24 @@ export const getSpec = ( }, complex: 'leaf', }, + [NoteNode.NoteContent]: { + content: '(block | paragraph)+', + group: 'block yfm-note', + parseDOM: [ + { + tag: 'div.yfm-note-content', + priority: 100, + }, + ], + toDOM() { + return ['div', {class: 'yfm-note-content'}, 0]; + }, + selectable: false, + allowSelection: false, + placeholder: { + content: placeholder?.[NoteNode.NoteContent] ?? DEFAULT_CONTENT_PLACEHOLDER, + alwaysVisible: true, + }, + complex: 'leaf', + }, }); diff --git a/src/extensions/yfm/YfmNote/YfmNoteSpecs/toYfm.ts b/src/extensions/yfm/YfmNote/YfmNoteSpecs/toYfm.ts index db8d233e..068959cb 100644 --- a/src/extensions/yfm/YfmNote/YfmNoteSpecs/toYfm.ts +++ b/src/extensions/yfm/YfmNote/YfmNoteSpecs/toYfm.ts @@ -1,3 +1,4 @@ +import {isNodeEmpty} from '../../../../utils/nodes'; import type {SerializerNodeToken} from '../../../../core'; import {getPlaceholderContent} from '../../../../utils/placeholder'; import {NoteAttrs, NoteNode} from './const'; @@ -23,4 +24,9 @@ export const toYfm: Record = { state.write('\n'); state.closeBlock(); }, + + [NoteNode.NoteContent]: (state, node) => { + if (!isNodeEmpty(node)) state.renderInline(node); + else state.write(getPlaceholderContent(node) + '\n\n'); + }, }; diff --git a/src/extensions/yfm/YfmNote/YfmNoteSpecs/utils.ts b/src/extensions/yfm/YfmNote/YfmNoteSpecs/utils.ts index 29dce22c..4e674fbc 100644 --- a/src/extensions/yfm/YfmNote/YfmNoteSpecs/utils.ts +++ b/src/extensions/yfm/YfmNote/YfmNoteSpecs/utils.ts @@ -3,3 +3,4 @@ import {NoteNode} from './const'; export const noteType = nodeTypeFactory(NoteNode.Note); export const noteTitleType = nodeTypeFactory(NoteNode.NoteTitle); +export const noteContentType = nodeTypeFactory(NoteNode.NoteContent); diff --git a/src/extensions/yfm/YfmNote/actions/toYfmNote.ts b/src/extensions/yfm/YfmNote/actions/toYfmNote.ts index 4e896923..065d2214 100644 --- a/src/extensions/yfm/YfmNote/actions/toYfmNote.ts +++ b/src/extensions/yfm/YfmNote/actions/toYfmNote.ts @@ -3,7 +3,7 @@ import {Command, EditorState} from 'prosemirror-state'; import {findParentNodeOfType, hasParentNodeOfType} from 'prosemirror-utils'; import type {ActionSpec} from '../../../../core'; import {NoteAttrs} from '../const'; -import {noteTitleType, noteType} from '../utils'; +import {noteContentType, noteTitleType, noteType} from '../utils'; export function isInsideYfmNote(state: EditorState) { return hasParentNodeOfType(noteType(state.schema))(state.selection); @@ -17,7 +17,10 @@ const createYfmNoteNode = (schema: Schema) => (type: YfmNoteType, content: Node [NoteAttrs.Class]: `yfm-note yfm-accent-${type}`, [NoteAttrs.Type]: type, }, - [noteTitleType(schema).createAndFill()!].concat(content), + [ + noteTitleType(schema).createAndFill()!, + noteContentType(schema).createAndFill({}, content)!, + ], ); };