mirror of
https://github.com/hcengineering/platform.git
synced 2025-06-11 21:11:57 +00:00
Use consistent handling of embed nodes in markdown and html
Signed-off-by: Nikolay Marchuk <nikolay.marchuk@hardcoreeng.com>
This commit is contained in:
parent
ca31a6b033
commit
0854ca9958
@ -41,7 +41,8 @@ export enum MarkupNodeType {
|
|||||||
table_header = 'tableHeader',
|
table_header = 'tableHeader',
|
||||||
mermaid = 'mermaid',
|
mermaid = 'mermaid',
|
||||||
comment = 'comment',
|
comment = 'comment',
|
||||||
markdown = 'markdown'
|
markdown = 'markdown',
|
||||||
|
embed = 'embed'
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @public */
|
/** @public */
|
||||||
|
@ -431,6 +431,87 @@ const tests: Array<{ name: string, markup: object, html: string }> = [
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
html: '<p>hello <span data-type="reference" data-id="64708c79c8f2613474dea38b" data-objectclass="contact:class:Person" data-label="John Doe">@John Doe</span></p>'
|
html: '<p>hello <span data-type="reference" data-id="64708c79c8f2613474dea38b" data-objectclass="contact:class:Person" data-label="John Doe">@John Doe</span></p>'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'embed',
|
||||||
|
markup: {
|
||||||
|
type: 'doc',
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'paragraph',
|
||||||
|
attrs: {
|
||||||
|
textAlign: null
|
||||||
|
},
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: 'hello '
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'embed',
|
||||||
|
attrs: {
|
||||||
|
src: 'http://localhost/embed'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
html: '<p>hello <a href="http://localhost/embed" data-type="embed">http://localhost/embed</a></p>'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'embed-uri-escape',
|
||||||
|
markup: {
|
||||||
|
type: 'doc',
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'paragraph',
|
||||||
|
attrs: {
|
||||||
|
textAlign: null
|
||||||
|
},
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: 'hello '
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'embed',
|
||||||
|
attrs: {
|
||||||
|
src: 'http://localhost/embed spaces'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
html: '<p>hello <a href="http://localhost/embed%20spaces" data-type="embed">http://localhost/embed spaces</a></p>'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'embed-html-escape',
|
||||||
|
markup: {
|
||||||
|
type: 'doc',
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'paragraph',
|
||||||
|
attrs: {
|
||||||
|
textAlign: null
|
||||||
|
},
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: 'hello '
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'embed',
|
||||||
|
attrs: {
|
||||||
|
src: 'http://localhost/embed<html>'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
html: '<p>hello <a href="http://localhost/embed%3Chtml%3E" data-type="embed">http://localhost/embed<html></a></p>'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -199,15 +199,6 @@ const styleRules: HtmlStyleRule[] = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
const markRules: Record<string, HtmlMarkRule> = {
|
const markRules: Record<string, HtmlMarkRule> = {
|
||||||
a: {
|
|
||||||
mark: MarkupMarkType.link,
|
|
||||||
getAttrs: (attributes: Record<string, string>) => {
|
|
||||||
return {
|
|
||||||
href: attributes.href,
|
|
||||||
title: attributes.title
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
b: {
|
b: {
|
||||||
mark: MarkupMarkType.bold
|
mark: MarkupMarkType.bold
|
||||||
},
|
},
|
||||||
@ -415,6 +406,30 @@ const specialRules: Record<string, HtmlSpecialRule> = {
|
|||||||
state.closeMark(MarkupMarkType.code)
|
state.closeMark(MarkupMarkType.code)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
a: {
|
||||||
|
handleOpenTag: (state: HtmlParseState, tag: string, attributes: Record<string, string>) => {
|
||||||
|
const dataType = attributes['data-type']
|
||||||
|
if (dataType === 'embed') {
|
||||||
|
state.openNode(MarkupNodeType.embed, {
|
||||||
|
src: decodeURI(attributes.href)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
state.openMark(MarkupMarkType.link, {
|
||||||
|
href: attributes.href,
|
||||||
|
title: attributes.title
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handleCloseTag: (state: HtmlParseState, tag: string) => {
|
||||||
|
const top = state.top()
|
||||||
|
if (top?.type === MarkupNodeType.embed) {
|
||||||
|
delete top.content
|
||||||
|
state.closeNode(MarkupNodeType.embed)
|
||||||
|
} else {
|
||||||
|
state.closeMark(MarkupMarkType.link)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -281,6 +281,11 @@ function addNodeContent (builder: NodeBuilder, node?: MarkupNode): void {
|
|||||||
builder.addText('<!-- ')
|
builder.addText('<!-- ')
|
||||||
addNodes(builder, nodes)
|
addNodes(builder, nodes)
|
||||||
builder.addText(' -->')
|
builder.addText(' -->')
|
||||||
|
} else if (node.type === MarkupNodeType.embed) {
|
||||||
|
const src = toString(attrs.src) ?? ''
|
||||||
|
builder.openTag('a', { href: encodeURI(src), 'data-type': 'embed' })
|
||||||
|
builder.addText(escapeHtml(src))
|
||||||
|
builder.closeTag('a')
|
||||||
} else {
|
} else {
|
||||||
// Handle unknown node types as div with data attribute
|
// Handle unknown node types as div with data attribute
|
||||||
builder.openTag('div', { 'data-node-type': node.type })
|
builder.openTag('div', { 'data-node-type': node.type })
|
||||||
|
@ -13,7 +13,6 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
//
|
//
|
||||||
|
|
||||||
import { MarkupNode } from '@hcengineering/text-core'
|
|
||||||
import { markdownToMarkup, markupToMarkdown } from '..'
|
import { markdownToMarkup, markupToMarkdown } from '..'
|
||||||
import { isMarkdownsEquals } from '../compare'
|
import { isMarkdownsEquals } from '../compare'
|
||||||
|
|
||||||
@ -774,6 +773,65 @@ Lorem ipsum dolor sit amet.
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'embed',
|
||||||
|
markdown: '<a href="http://localhost/embed" data-type="embed">http://localhost/embed</a>',
|
||||||
|
markup: {
|
||||||
|
type: 'doc',
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'paragraph',
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'embed',
|
||||||
|
attrs: { src: 'http://localhost/embed' },
|
||||||
|
content: []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'embed-uri-escape',
|
||||||
|
markdown:
|
||||||
|
'<a href="http://localhost/embed%20spaces" data-type="embed">http://localhost/embed spaces</a>',
|
||||||
|
markup: {
|
||||||
|
type: 'doc',
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'paragraph',
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'embed',
|
||||||
|
attrs: { src: 'http://localhost/embed spaces' },
|
||||||
|
content: []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'embed-html-escape',
|
||||||
|
markdown:
|
||||||
|
'<a href="http://localhost/embed%3Chtml%3E" data-type="embed">http://localhost/embed<html></a>',
|
||||||
|
markup: {
|
||||||
|
type: 'doc',
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'paragraph',
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'embed',
|
||||||
|
attrs: { src: 'http://localhost/embed<html>' },
|
||||||
|
content: []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -851,61 +909,3 @@ describe('markdownToMarkup -> markupToMarkdown', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('markupToMarkdown', () => {
|
|
||||||
const tests: Array<{ name: string, markup: object, markdown: string }> = [
|
|
||||||
{
|
|
||||||
name: 'embed',
|
|
||||||
markup: {
|
|
||||||
type: 'doc',
|
|
||||||
content: [
|
|
||||||
{
|
|
||||||
type: 'embed',
|
|
||||||
attrs: { src: 'http://localhost/embed' },
|
|
||||||
content: []
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
markdown: '<http://localhost/embed>'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'embed-spaces',
|
|
||||||
markup: {
|
|
||||||
type: 'doc',
|
|
||||||
content: [
|
|
||||||
{
|
|
||||||
type: 'embed',
|
|
||||||
attrs: { src: 'http://localhost/embed spaces' },
|
|
||||||
content: []
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
markdown: '[http://localhost/embed spaces](http://localhost/embed%20spaces)'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'embed-sub',
|
|
||||||
markup: {
|
|
||||||
type: 'doc',
|
|
||||||
content: [
|
|
||||||
{
|
|
||||||
type: 'subLink',
|
|
||||||
content: [
|
|
||||||
{
|
|
||||||
type: 'embed',
|
|
||||||
attrs: { src: 'http://localhost/embed?c=<a>' },
|
|
||||||
content: []
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
markdown: '<sub><a href="http://localhost/embed?c=%3Ca%3E">http://localhost/embed?c=<a></a></sub>'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
tests.forEach(({ name, markup, markdown }) => {
|
|
||||||
it(name, () => {
|
|
||||||
const serialized = markupToMarkdown(markup as MarkupNode, options)
|
|
||||||
expect(serialized).toEqual(markdown)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
@ -332,16 +332,26 @@ function tokenHandlers (
|
|||||||
|
|
||||||
handlers.html_inline = (state: MarkdownParseState, tok: Token) => {
|
handlers.html_inline = (state: MarkdownParseState, tok: Token) => {
|
||||||
try {
|
try {
|
||||||
|
const top = state.top()
|
||||||
|
if (tok.content.trim() === '</a>' && top?.type === MarkupNodeType.embed) {
|
||||||
|
top.content = []
|
||||||
|
state.closeNode()
|
||||||
|
return
|
||||||
|
}
|
||||||
const markup = htmlParser(tok.content)
|
const markup = htmlParser(tok.content)
|
||||||
if (markup.content !== undefined) {
|
if (markup.content !== undefined) {
|
||||||
// unwrap content from wrapping paragraph
|
// unwrap content from wrapping paragraph
|
||||||
const shouldUnwrap =
|
const shouldUnwrap =
|
||||||
markup.content.length === 1 &&
|
markup.content.length === 1 &&
|
||||||
markup.content[0].type === MarkupNodeType.paragraph &&
|
markup.content[0].type === MarkupNodeType.paragraph &&
|
||||||
state.top()?.type === MarkupNodeType.paragraph
|
top?.type === MarkupNodeType.paragraph
|
||||||
|
|
||||||
const content = nodeContent(shouldUnwrap ? markup.content[0] : markup)
|
const content = nodeContent(shouldUnwrap ? markup.content[0] : markup)
|
||||||
for (const c of content) {
|
for (const c of content) {
|
||||||
|
if (c.type === MarkupNodeType.embed) {
|
||||||
|
state.openNode(MarkupNodeType.embed, c.attrs ?? {})
|
||||||
|
continue
|
||||||
|
}
|
||||||
state.push(c)
|
state.push(c)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -278,16 +278,10 @@ export const storeNodes: Record<string, NodeProcessor> = {
|
|||||||
embed: (state, node) => {
|
embed: (state, node) => {
|
||||||
const attrs = nodeAttrs(node)
|
const attrs = nodeAttrs(node)
|
||||||
const embedUrl = attrs.src as string
|
const embedUrl = attrs.src as string
|
||||||
if (state.renderAHref === true) {
|
state.write(`<a href="${encodeURI(embedUrl)}" data-type="embed">`)
|
||||||
state.write(`<a href="${encodeURI(embedUrl)}">${state.htmlEsc(embedUrl)}</a>`)
|
// Slashes are escaped to prevent autolink creation
|
||||||
} else {
|
state.write(state.htmlEsc(embedUrl).replace(/\//g, '/'))
|
||||||
const encoded = encodeURI(embedUrl)
|
state.write('</a>')
|
||||||
if (encoded === embedUrl) {
|
|
||||||
state.write(`<${state.esc(embedUrl)}>`)
|
|
||||||
} else {
|
|
||||||
state.write(`[${state.esc(embedUrl)}](${encoded})`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user