mirror of
https://github.com/hcengineering/platform.git
synced 2025-06-09 09:20:54 +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',
|
||||
mermaid = 'mermaid',
|
||||
comment = 'comment',
|
||||
markdown = 'markdown'
|
||||
markdown = 'markdown',
|
||||
embed = 'embed'
|
||||
}
|
||||
|
||||
/** @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>'
|
||||
},
|
||||
{
|
||||
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> = {
|
||||
a: {
|
||||
mark: MarkupMarkType.link,
|
||||
getAttrs: (attributes: Record<string, string>) => {
|
||||
return {
|
||||
href: attributes.href,
|
||||
title: attributes.title
|
||||
}
|
||||
}
|
||||
},
|
||||
b: {
|
||||
mark: MarkupMarkType.bold
|
||||
},
|
||||
@ -415,6 +406,30 @@ const specialRules: Record<string, HtmlSpecialRule> = {
|
||||
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('<!-- ')
|
||||
addNodes(builder, nodes)
|
||||
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 {
|
||||
// Handle unknown node types as div with data attribute
|
||||
builder.openTag('div', { 'data-node-type': node.type })
|
||||
|
@ -13,7 +13,6 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import { MarkupNode } from '@hcengineering/text-core'
|
||||
import { markdownToMarkup, markupToMarkdown } from '..'
|
||||
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) => {
|
||||
try {
|
||||
const top = state.top()
|
||||
if (tok.content.trim() === '</a>' && top?.type === MarkupNodeType.embed) {
|
||||
top.content = []
|
||||
state.closeNode()
|
||||
return
|
||||
}
|
||||
const markup = htmlParser(tok.content)
|
||||
if (markup.content !== undefined) {
|
||||
// unwrap content from wrapping paragraph
|
||||
const shouldUnwrap =
|
||||
markup.content.length === 1 &&
|
||||
markup.content[0].type === MarkupNodeType.paragraph &&
|
||||
state.top()?.type === MarkupNodeType.paragraph
|
||||
top?.type === MarkupNodeType.paragraph
|
||||
|
||||
const content = nodeContent(shouldUnwrap ? markup.content[0] : markup)
|
||||
for (const c of content) {
|
||||
if (c.type === MarkupNodeType.embed) {
|
||||
state.openNode(MarkupNodeType.embed, c.attrs ?? {})
|
||||
continue
|
||||
}
|
||||
state.push(c)
|
||||
}
|
||||
}
|
||||
|
@ -278,16 +278,10 @@ export const storeNodes: Record<string, NodeProcessor> = {
|
||||
embed: (state, node) => {
|
||||
const attrs = nodeAttrs(node)
|
||||
const embedUrl = attrs.src as string
|
||||
if (state.renderAHref === true) {
|
||||
state.write(`<a href="${encodeURI(embedUrl)}">${state.htmlEsc(embedUrl)}</a>`)
|
||||
} else {
|
||||
const encoded = encodeURI(embedUrl)
|
||||
if (encoded === embedUrl) {
|
||||
state.write(`<${state.esc(embedUrl)}>`)
|
||||
} else {
|
||||
state.write(`[${state.esc(embedUrl)}](${encoded})`)
|
||||
}
|
||||
}
|
||||
state.write(`<a href="${encodeURI(embedUrl)}" data-type="embed">`)
|
||||
// Slashes are escaped to prevent autolink creation
|
||||
state.write(state.htmlEsc(embedUrl).replace(/\//g, '/'))
|
||||
state.write('</a>')
|
||||
}
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user