Skip to content

Commit

Permalink
support merging of table cells (#899)
Browse files Browse the repository at this point in the history
* parse attributes in shortcodes

* add support for colspan and rowspan attributes in tables

* remove errant logging statements

* fix issue with attributes in turndown rule

* add some comments to explain the new ckeditor constants

* add tests for rowspan, colspan and attribute whitelist
  • Loading branch information
gumaerc authored Jan 24, 2022
1 parent 928ff48 commit b96926d
Show file tree
Hide file tree
Showing 5 changed files with 136 additions and 12 deletions.
27 changes: 19 additions & 8 deletions static/js/lib/ckeditor/plugins/Markdown.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@ import GFMDataProcessor from "@ckeditor/ckeditor5-markdown-gfm/src/gfmdataproces
import { editor } from "@ckeditor/ckeditor5-core"

import MarkdownConfigPlugin from "./MarkdownConfigPlugin"
import { ATTRIBUTE_REGEX } from "./constants"

import { turndownService } from "../turndown"
import { buildAttrsString } from "./util"

/**
* Data processor for CKEditor which implements conversion to / from Markdown
Expand Down Expand Up @@ -63,8 +65,8 @@ export class MarkdownDataProcessor extends GFMDataProcessor {
}
}

const TD_CONTENT_REGEX = /<td>([\S\s]*?)<\/td>/g
const TH_CONTENT_REGEX = /<th>([\S\s]*?)<\/th>/g
const TD_CONTENT_REGEX = /<td.*?>([\S\s]*?)<\/td>/g
const TH_CONTENT_REGEX = /<th(?!ead).*?>([\S\s]*?)<\/th>/g

/**
* Plugin implementing Markdown for CKEditor
Expand Down Expand Up @@ -94,16 +96,25 @@ export default class Markdown extends MarkdownConfigPlugin {
turndownService._customRulesSet = true
}

function formatTableCell(
el: string,
html: string,
contents: string
): string {
const attrs = html.match(ATTRIBUTE_REGEX)
return `<${el}${buildAttrsString(attrs)}>${converter.makeHtml(
contents
)}</${el}>`
}

function md2html(md: string): string {
return converter
.makeHtml(md)
.replace(
TD_CONTENT_REGEX,
(_match, contents) => `<td>${converter.makeHtml(contents)}</td>`
.replace(TD_CONTENT_REGEX, (_match, contents) =>
formatTableCell("td", _match, contents)
)
.replace(
TH_CONTENT_REGEX,
(_match, contents) => `<th>${converter.makeHtml(contents)}</th>`
.replace(TH_CONTENT_REGEX, (_match, contents) =>
formatTableCell("th", _match, contents)
)
}

Expand Down
74 changes: 74 additions & 0 deletions static/js/lib/ckeditor/plugins/TableMarkdownSyntax.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ jest.mock("@ckeditor/ckeditor5-utils/src/version")
import { equals } from "ramda"
import Markdown from "./Markdown"
import { createTestEditor, markdownTest } from "./test_util"
import { html_beautify as htmlBeautify } from "js-beautify"
import { MarkdownDataProcessor } from "./Markdown"
import { turndownService } from "../turndown"
import ParagraphPlugin from "@ckeditor/ckeditor5-paragraph/src/paragraph"

Expand Down Expand Up @@ -81,4 +83,76 @@ describe("table shortcodes", () => {
</table>`
)
})

it("should transform a table with colspan and rowspan attributes", async () => {
const editor = await getEditor("")
markdownTest(
editor,
`{{< tableopen >}}{{< theadopen >}}{{< tropen >}}{{< thopen colspan="2" >}}\n**A title row**\n{{< thclose >}}{{< trclose >}}{{< theadclose >}}{{< tbodyopen >}}{{< tropen >}}{{< tdopen rowspan="2" >}}\nrowspan test\n{{< tdclose >}}{{< tdopen >}}\nrowspan 1\n{{< tdclose >}}{{< trclose >}}{{< tropen >}}{{< tdopen >}}\nrowspan 2\n{{< tdclose >}}{{< trclose >}}{{< tbodyclose >}}{{< tableclose >}}`,
`<table>
<thead>
<tr>
<th colspan="2">
<p><strong>A title row</strong></p>
</th>
</tr>
</thead>
<tbody>
<tr>
<td rowspan="2">
<p>rowspan test</p>
</td>
<td>
<p>rowspan 1</p>
</td>
</tr>
<tr>
<td>
<p>rowspan 2</p>
</td>
</tr>
</tbody>
</table>`
)
})

it("ignores attributes on table cells that are not in the whitelist", async () => {
const editor = await getEditor("")
const { md2html, html2md } = (editor.data
.processor as unknown) as MarkdownDataProcessor
let html = `<table>
<thead>
<tr>
<th colspan="2">
<p><strong>A title row</strong></p>
</th>
</tr>
</thead>
<tbody>
<tr>
<td class="sneaky-css-class"><p>data</p></td>
<td><p>data</p></td>
</tr>
</tbody>
</table>`
let md = `{{< tableopen >}}{{< theadopen >}}{{< tropen >}}{{< thopen colspan="2" >}}\n**A title row**\n{{< thclose >}}{{< trclose >}}{{< theadclose >}}{{< tbodyopen >}}{{< tropen >}}{{< tdopen >}}\ndata\n{{< tdclose >}}{{< tdopen >}}\ndata\n{{< tdclose >}}{{< trclose >}}{{< tbodyclose >}}{{< tableclose >}}`
expect(html2md(html)).toBe(md)
md = `{{< tableopen >}}{{< theadopen >}}{{< tropen >}}{{< thopen colspan="2" >}}\n**A title row**\n{{< thclose >}}{{< trclose >}}{{< theadclose >}}{{< tbodyopen >}}{{< tropen >}}{{< tdopen class="sneaky-css-class" >}}\ndata\n{{< tdclose >}}{{< tdopen >}}\ndata\n{{< tdclose >}}{{< trclose >}}{{< tbodyclose >}}{{< tableclose >}}`
html = `<table>
<thead>
<tr>
<th colspan="2">
<p><strong>A title row</strong></p>
</th>
</tr>
</thead>
<tbody>
<tr>
<td><p>data</p></td>
<td><p>data</p></td>
</tr>
</tbody>
</table>`
expect(htmlBeautify(md2html(md))).toBe(htmlBeautify(html))
})
})
22 changes: 18 additions & 4 deletions static/js/lib/ckeditor/plugins/TableMarkdownSyntax.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@ import Showdown from "showdown"

import MarkdownSyntaxPlugin from "./MarkdownSyntaxPlugin"
import { TurndownRule } from "../../../types/ckeditor_markdown"
import { buildAttrsString } from "./util"

import { TABLE_ELS } from "./constants"
import { TABLE_ELS, ATTRIBUTE_REGEX } from "./constants"

type Position = "open" | "close"

Expand All @@ -16,13 +17,16 @@ export default class TableMarkdownSyntax extends MarkdownSyntaxPlugin {
get showdownExtension() {
return function resourceExtension(): Showdown.ShowdownExtension[] {
return TABLE_ELS.map(el => {
const shortcodeRegex = new RegExp(`{{< ${el}(open|close) >}}`, "g")
const shortcodeRegex = new RegExp(`{{< ${el}(open|close).*? >}}`, "g")

return {
type: "lang",
regex: shortcodeRegex,
replace: (_s: string, position: Position) => {
return position === "open" ? `<${el}>` : `</${el}>`
const attrs = _s.match(ATTRIBUTE_REGEX)
return position === "open" ?
`<${el}${buildAttrsString(attrs)}>` :
`</${el}>`
}
}
})
Expand All @@ -37,7 +41,17 @@ export default class TableMarkdownSyntax extends MarkdownSyntaxPlugin {
replacement: (content: string, node: Turndown.Node): string => {
const name = node.nodeName.toLowerCase()
const normalizedContent = content.replace("\n\n", "\n")
return `{{< ${name}open >}}${normalizedContent}{{< ${name}close >}}`
//@ts-ignore
const attributes = node.hasAttributes() ?
buildAttrsString(
//@ts-ignore
Array.from(node.attributes).map(
//@ts-ignore
attr => `${attr.name}="${attr.value}"`
)
) :
""
return `{{< ${name}open${attributes} >}}${normalizedContent}{{< ${name}close >}}`
}
}
}
Expand Down
12 changes: 12 additions & 0 deletions static/js/lib/ckeditor/plugins/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,3 +52,15 @@ export const TABLE_ELS: TurndownService.TagName[] = [
"thead",
"tfoot"
]

// A whitelist of attributes that can be assigned to table cells
export const TABLE_ALLOWED_ATTRS: string[] = ["colspan", "rowspan"]

/**
* A regex designed to extract attributes from html tags or shortcodes
*
* It starts with matching 1 or more of anything but whitespace, then
* an equals sign followed by a single or double quote. The regex ends
* with a double quote and captures anything in between the quotes.
*/
export const ATTRIBUTE_REGEX = /(\S+)=["']?((?:.(?!["']?\s+(?:\S+)=|\s*\/?[>"']))+.)["']?/g
13 changes: 13 additions & 0 deletions static/js/lib/ckeditor/plugins/util.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { TABLE_ALLOWED_ATTRS } from "./constants"

export function buildAttrsString(attrs: RegExpMatchArray | null): string {
return attrs ?
attrs
.map(attr =>
TABLE_ALLOWED_ATTRS.some(allowedAttr => attr.includes(allowedAttr)) ?
` ${attr}` :
""
)
.join("") :
""
}

0 comments on commit b96926d

Please sign in to comment.