Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .changeset/khaki-snakes-sing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
'prosemirror-transformer-markdown': patch
'@prosedoc/markdown-schema': patch
'@better-comments-for-github/extension': patch
'@better-comments-for-github/core': patch
---

Improve table cell with line breaks support and fix rendering on markdown
2 changes: 2 additions & 0 deletions markdown-schema/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,12 @@
"happy-dom": "^18.0.1",
"hast-util-to-html": "^9.0.5",
"mdast-util-from-markdown": "^2.0.2",
"mdast-util-to-hast": "^13.2.1",
"mdast-util-to-markdown": "^2.1.2",
"micromark-util-types": "^2.0.2",
"prosekit": "catalog:",
"prosemirror-inputrules": "catalog:",
"prosemirror-model": "catalog:",
"prosemirror-tables": "^1.8.1",
"prosemirror-transformer-markdown": "workspace:*",
"remark-comment-config": "^8.0.0",
Expand Down
54 changes: 54 additions & 0 deletions markdown-schema/src/table/remarkTableOutputHtml.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/*
* Copyright 2025 Riccardo Perra
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import { EXIT, visit } from 'unist-util-visit'
import { toHast } from 'mdast-util-to-hast'
import { toHtml } from 'hast-util-to-html'
import type { PhrasingContent, Root } from 'mdast'

const nodeTypes: Array<string> = [
'text',
'break',
'link',
'inlineCode',
'strong',
] satisfies Array<PhrasingContent['type']>

export function remarkTableToHtmlOnComplexContent() {
return (tree: Root) => {
visit(tree, 'table', (node, index, parent) => {
let complex = false

visit(node, 'tableCell', (cell) => {
visit(cell, function (node) {
if (node.type !== 'tableCell' && !nodeTypes.includes(node.type)) {
complex = true
return EXIT
}
if (complex) {
return EXIT
}
})
})

if (complex && parent && typeof index === 'number') {
const hast = toHast(node)
const html = toHtml(hast)
parent.children[index] = { type: 'html', value: html }
}
})
}
}
135 changes: 129 additions & 6 deletions markdown-schema/src/table/table.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,20 +28,30 @@ import {
sameNode,
testUnknownHandler,
} from '../test-utils'
import {
defineHardbreakMarkdown,
remarkHtmlHardbreak,
} from '../hardbreak/hardbreak'
import { defineListMarkdown, unistMergeAdjacentList } from '../list/list'
import { defineTableMarkdown } from './table'
import { remarkTableToHtmlOnComplexContent } from './remarkTableOutputHtml'

const extension = getMarksBaseExtensions([defineTableMarkdown()])
const extension = getMarksBaseExtensions([
defineTableMarkdown(),
defineHardbreakMarkdown(),
defineListMarkdown(),
])

const { doc, p, table, tableHeaderCell, tableRow, tableCell } = builders(
extension.schema!,
{
const { doc, p, table, tableHeaderCell, tableRow, tableCell, br, list } =
builders(extension.schema!, {
p: { nodeType: 'paragraph' },
br: { nodeType: 'hardBreak' },
table: { markType: 'table' },
tableHeaderCell: { markType: 'tableHeaderCell' },
tableRow: { markType: 'tableRow' },
tableCell: { markType: 'tableCell' },
},
)
list: { nodeType: 'list' },
})

test('markdown -> prosemirror', () => {
const editor = getEditorInstance(extension)
Expand Down Expand Up @@ -110,3 +120,116 @@ test('prosemirror -> markdown', () => {
| Content Cell 3 | Content Cell 4 |`,
)
})

// https://github.com/riccardoperra/better-comments-for-github/issues/86
test('parse content with line breaks', () => {
const editor = getEditorInstance(extension)

const unist = markdownToUnist(
`| Column 1 | Column 2 |
|--------|--------|
| This cell has a<br>line break in it | This cell does not |
| Still nothing in this cell | 1. This cell uses line breaks<br>2. to appear as a numbered list |
`,
{
transformers: [remarkHtmlHardbreak],
},
)

const result = convertUnistToProsemirror(
unist,
editor.schema,
testUnknownHandler,
)

sameNode(
result,
doc(
table(
tableRow(
tableHeaderCell(p('Column 1')),
tableHeaderCell(p('Column 2')),
),
tableRow(
tableCell(p('This cell has a', br(), 'line break in it')),
tableCell(p('This cell does not')),
),
tableRow(
tableCell(p('Still nothing in this cell')),
tableCell(
p(
'1. This cell uses line breaks',
br(),
'2. to appear as a numbered list',
),
),
),
),
),
)

sameMarkdown(
convertPmSchemaToUnist(result, editor.schema),
`| Column 1 | Column 2 |
| ----------------------------------- | ---------------------------------------------------------------- |
| This cell has a<br>line break in it | This cell does not |
| Still nothing in this cell | 1. This cell uses line breaks<br>2. to appear as a numbered list |`,
)
})

test('render as html tag when includes non-phrasing content', () => {
const editor = getEditorInstance(
extension,
doc(
table(
tableRow(
tableHeaderCell(p('First Header')),
tableHeaderCell(p('Second Header')),
),
tableRow(
tableCell(p('Content Cell 1')),
tableCell(
list({ kind: 'bullet' }, p('First item')),
list({ kind: 'bullet' }, p('Second item')),
),
),
tableRow(
tableCell(p('Content Cell 3')),
tableCell(p('Content Cell 4')),
),
),
),
)

const result = convertPmSchemaToUnist(editor.state.doc, editor.schema, {
postProcess: (node) => {
unistMergeAdjacentList(node)
remarkTableToHtmlOnComplexContent()(node)
},
})

sameMarkdown(
result,
`<table>
<thead>
<tr>
<th><p>First Header</p></th>
<th><p>Second Header</p></th>
</tr>
</thead>
<tbody>
<tr>
<td>Content Cell 1</td>
<td><ul>
<li>First item</li>
<li>Second item</li>
</ul></td>
</tr>
<tr>
<td>Content Cell 3</td>
<td>Content Cell 4</td>
</tr>
</tbody>
</table>`,
)
})
47 changes: 36 additions & 11 deletions markdown-schema/src/table/table.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,17 @@ import {
defineTableRowSpec,
defineTableSpec,
} from 'prosekit/extensions/table'
import { createProseMirrorNode } from 'prosemirror-transformer-markdown/prosemirror'
import {
fromProseMirrorNode,
toProseMirrorNode,
} from '@prosemirror-processor/unist/mdast'
import { pmNode } from '@prosemirror-processor/unist'
import { tableEditing } from 'prosemirror-tables'
import type { Parent } from 'mdast'
import { Fragment, Slice } from 'prosemirror-model'
import { Transform } from 'prosekit/pm/transform'
import type { Parent, PhrasingContent } from 'mdast'

export { remarkTableToHtmlOnComplexContent } from './remarkTableOutputHtml'

export function defineTableMarkdown() {
return union(
Expand All @@ -47,24 +50,46 @@ export function defineTableMarkdown() {
const isHead =
!!parent &&
parent.type === 'tableRow' &&
node.type === 'tableCell' &&
// TODO: should access to the parent of table row in my opinion
parent.position?.start.line === 1 &&
node.position?.start.line === 1
const children = context.handleAll(node as Parent)
const mappedChildren = children.map((child) => {
if (child.isText) {
return createProseMirrorNode('paragraph', context.schema, [
child,
])[0]
}
return child
})
const fragment = Fragment.from(children)

const nodeType = isHead
? context.schema.nodes.tableHeaderCell
: context.schema.nodes.tableCell
return pmNode(nodeType, mappedChildren, null)

if (nodeType.validContent(fragment)) {
return pmNode(nodeType, fragment, null, { mode: 'fill' })
}

const cellNode = pmNode(nodeType, [], null, { mode: 'fill' })!
// Fix https://github.com/riccardoperra/better-comments-for-github/issues/86
// When the given fragment is not valid, instead of writing our-self the logic to match
// the required content using contentMatch, we instead use pm transform with `fitter` logic
// to automatically wrap the content into something that can be accepted by the cell content.
// In most of the cases, this should create a paragraph that wraps the parsed children.
const tr = new Transform(cellNode)
tr.replace(1, 1, new Slice(fragment, 0, 0))
return tr.doc
},
__toUnist: (node, parent, context) => {
const { content } = node

if (
content.childCount === 1 &&
content.child(0).type.name === 'paragraph'
) {
const p = content.child(0)
const childNodes = context.handleAll(p)
return {
type: 'tableCell',
children: childNodes as unknown as Array<PhrasingContent>,
} as const
}

return fromProseMirrorNode('tableCell')(node, parent, context as any)
},
unistName: 'tableCell',
Expand Down
8 changes: 4 additions & 4 deletions markdown-transformer/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,13 @@
"exports": {
"./unified": {
"source": "./src/unified/index.ts",
"types": "./dist/unified/index.d.ts",
"import": "./dist/unified/index.js"
"types": "./dist/unified/index.d.mts",
"import": "./dist/unified/index.mjs"
},
"./prosemirror": {
"source": "./src/prosemirror/index.ts",
"types": "./dist/prosemirror/index.d.ts",
"import": "./dist/prosemirror/index.js"
"types": "./dist/prosemirror/index.d.mts",
"import": "./dist/prosemirror/index.mjs"
}
},
"scripts": {
Expand Down
10 changes: 9 additions & 1 deletion markdown-transformer/src/unified/markdownFromUnistNode.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { Root } from "mdast";
import { unistToMarkdown } from "@prosemirror-processor/markdown";
import { defaultHandlers } from "mdast-util-to-markdown";

export function markdownFromUnistNode(rootNode: Root): string {
return unistToMarkdown(rootNode, {
Expand All @@ -12,8 +13,15 @@ export function markdownFromUnistNode(rootNode: Root): string {
emphasis: "*",
incrementListMarker: true,
rule: "-",
// ruleSpaces: true,
strong: "*",
handlers: {
break: (node, parent, state, info) => {
if (parent && parent.type === "tableCell") {
return "<br>";
}
return defaultHandlers.break(node, parent, state, info);
},
},
},
});
}
8 changes: 5 additions & 3 deletions markdown-transformer/tsdown.config.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { defineConfig } from "tsdown";
import type { Options } from "tsdown";
import type { UserConfig } from "tsdown";

const config: Options[] = defineConfig([
const options: UserConfig[] = [
{
name: "Transformer/Unified",
clean: true,
Expand All @@ -18,6 +18,8 @@ const config: Options[] = defineConfig([
dts: true,
format: "esm",
},
]) as Options[];
];

const config: UserConfig[] = defineConfig(options);

export default config;
Loading