@@ -26,6 +26,38 @@ import {
2626// JSON post-processing (see liftBlockImages / wrapBlockImages).
2727// ---------------------------------------------------------------------------
2828
29+ const tableCellAttrs = {
30+ colspan : { default : 1 } ,
31+ rowspan : { default : 1 } ,
32+ colwidth : { default : null } ,
33+ } ;
34+
35+ function getTableCellAttrs ( dom : Node | string ) {
36+ if ( typeof dom === "string" ) return { } ;
37+ const element = dom as HTMLElement ;
38+ const widthAttr = element . getAttribute ( "data-colwidth" ) ;
39+ const widths =
40+ widthAttr && / ^ \d + ( , \d + ) * $ / . test ( widthAttr )
41+ ? widthAttr . split ( "," ) . map ( ( value ) => Number ( value ) )
42+ : null ;
43+ const colspan = Number ( element . getAttribute ( "colspan" ) || 1 ) ;
44+
45+ return {
46+ colspan,
47+ rowspan : Number ( element . getAttribute ( "rowspan" ) || 1 ) ,
48+ colwidth : widths && widths . length === colspan ? widths : null ,
49+ } ;
50+ }
51+
52+ function setTableCellAttrs ( node : PMNode ) {
53+ const attrs : Record < string , string | number > = { } ;
54+ if ( node . attrs . colspan !== 1 ) attrs . colspan = node . attrs . colspan ;
55+ if ( node . attrs . rowspan !== 1 ) attrs . rowspan = node . attrs . rowspan ;
56+ if ( node . attrs . colwidth )
57+ attrs [ "data-colwidth" ] = node . attrs . colwidth . join ( "," ) ;
58+ return attrs ;
59+ }
60+
2961const nodes : Record < string , NodeSpec > = {
3062 doc : { content : "block+" } ,
3163
@@ -135,6 +167,48 @@ const nodes: Record<string, NodeSpec> = {
135167 } ,
136168 } ,
137169
170+ table : {
171+ content : "tableRow+" ,
172+ group : "block" ,
173+ isolating : true ,
174+ tableRole : "table" ,
175+ parseDOM : [ { tag : "table" } ] ,
176+ toDOM ( ) {
177+ return [ "table" , [ "tbody" , 0 ] ] ;
178+ } ,
179+ } ,
180+
181+ tableRow : {
182+ content : "(tableCell | tableHeader)*" ,
183+ tableRole : "row" ,
184+ parseDOM : [ { tag : "tr" } ] ,
185+ toDOM ( ) {
186+ return [ "tr" , 0 ] ;
187+ } ,
188+ } ,
189+
190+ tableCell : {
191+ content : "block+" ,
192+ attrs : tableCellAttrs ,
193+ isolating : true ,
194+ tableRole : "cell" ,
195+ parseDOM : [ { tag : "td" , getAttrs : getTableCellAttrs } ] ,
196+ toDOM ( node ) {
197+ return [ "td" , setTableCellAttrs ( node ) , 0 ] ;
198+ } ,
199+ } ,
200+
201+ tableHeader : {
202+ content : "block+" ,
203+ attrs : tableCellAttrs ,
204+ isolating : true ,
205+ tableRole : "header_cell" ,
206+ parseDOM : [ { tag : "th" , getAttrs : getTableCellAttrs } ] ,
207+ toDOM ( node ) {
208+ return [ "th" , setTableCellAttrs ( node ) , 0 ] ;
209+ } ,
210+ } ,
211+
138212 taskList : {
139213 content : "taskItem+" ,
140214 group : "block" ,
@@ -560,6 +634,39 @@ function taskListPlugin(md: MarkdownIt) {
560634 } ) ;
561635}
562636
637+ function tableCellParagraphsPlugin ( md : MarkdownIt ) {
638+ md . core . ruler . after ( "inline" , "table_cell_paragraphs" , ( state ) => {
639+ const tokens = state . tokens ;
640+ const out : Token [ ] = [ ] ;
641+
642+ for ( let i = 0 ; i < tokens . length ; i ++ ) {
643+ const token = tokens [ i ] ;
644+ const prev = tokens [ i - 1 ] ;
645+ const next = tokens [ i + 1 ] ;
646+
647+ if (
648+ token . type === "inline" &&
649+ ( prev ?. type === "th_open" || prev ?. type === "td_open" ) &&
650+ ( next ?. type === "th_close" || next ?. type === "td_close" )
651+ ) {
652+ const open = new state . Token ( "paragraph_open" , "p" , 1 ) ;
653+ open . level = token . level ;
654+ const inline = new state . Token ( "inline" , "" , 0 ) ;
655+ Object . assign ( inline , token ) ;
656+ inline . level = token . level + 1 ;
657+ const close = new state . Token ( "paragraph_close" , "p" , - 1 ) ;
658+ close . level = token . level ;
659+
660+ out . push ( open , inline , close ) ;
661+ } else {
662+ out . push ( token ) ;
663+ }
664+ }
665+
666+ state . tokens = out ;
667+ } ) ;
668+ }
669+
563670function findInlineToken ( tokens : Token [ ] , fromIdx : number ) : number {
564671 for ( let i = fromIdx + 1 ; i < tokens . length ; i ++ ) {
565672 if ( tokens [ i ] . type === "inline" ) return i ;
@@ -850,6 +957,8 @@ function getParser(): MarkdownParser {
850957 md . use ( strikethroughPlugin ) ;
851958 md . use ( underlinePlugin ) ;
852959 md . use ( highlightPlugin ) ;
960+ md . enable ( "table" ) ;
961+ md . use ( tableCellParagraphsPlugin ) ;
853962 md . use ( taskListPlugin ) ;
854963 md . use ( clipPlugin ) ;
855964 md . use ( fileAttachmentPlugin ) ;
@@ -891,6 +1000,12 @@ function getParser(): MarkdownParser {
8911000 } ,
8921001 } ,
8931002 hardbreak : { node : "hardBreak" } ,
1003+ table : { block : "table" } ,
1004+ thead : { ignore : true } ,
1005+ tbody : { ignore : true } ,
1006+ tr : { block : "tableRow" } ,
1007+ th : { block : "tableHeader" } ,
1008+ td : { block : "tableCell" } ,
8941009
8951010 em : { mark : "italic" } ,
8961011 strong : { mark : "bold" } ,
@@ -960,6 +1075,38 @@ function backticksFor(node: PMNode, side: number): string {
9601075 return result ;
9611076}
9621077
1078+ function escapeTableCell ( markdown : string ) : string {
1079+ return markdown
1080+ . replace ( / \n + / g, "<br>" )
1081+ . replace ( / \\ / g, "\\\\" )
1082+ . replace ( / \| / g, "\\|" )
1083+ . trim ( ) ;
1084+ }
1085+
1086+ function tableCellToMarkdown ( cell : PMNode ) : string {
1087+ const parts : string [ ] = [ ] ;
1088+
1089+ cell . forEach ( ( child ) => {
1090+ const doc = markdownSchema . node ( "doc" , null , [ child ] ) ;
1091+ parts . push ( getSerializer ( ) . serialize ( doc ) ) ;
1092+ } ) ;
1093+
1094+ return escapeTableCell ( parts . join ( "<br>" ) ) ;
1095+ }
1096+
1097+ function tableRowToMarkdown ( row : PMNode ) : string {
1098+ const cells : string [ ] = [ ] ;
1099+ row . forEach ( ( cell ) => {
1100+ cells . push ( tableCellToMarkdown ( cell ) ) ;
1101+ } ) ;
1102+
1103+ return `| ${ cells . join ( " | " ) } |` ;
1104+ }
1105+
1106+ function tableDelimiterRow ( columnCount : number ) : string {
1107+ return `| ${ Array . from ( { length : columnCount } , ( ) => "---" ) . join ( " | " ) } |` ;
1108+ }
1109+
9631110let _serializer : MarkdownSerializer | null = null ;
9641111
9651112function getSerializer ( ) : MarkdownSerializer {
@@ -1010,6 +1157,32 @@ function getSerializer(): MarkdownSerializer {
10101157 state . renderContent ( node ) ;
10111158 } ,
10121159
1160+ table ( state , node ) {
1161+ const rows : PMNode [ ] = [ ] ;
1162+ node . forEach ( ( row ) => rows . push ( row ) ) ;
1163+ if ( rows . length === 0 ) {
1164+ state . closeBlock ( node ) ;
1165+ return ;
1166+ }
1167+
1168+ state . write ( tableRowToMarkdown ( rows [ 0 ] ) ) ;
1169+ state . write ( "\n" ) ;
1170+ state . write ( tableDelimiterRow ( rows [ 0 ] . childCount ) ) ;
1171+
1172+ for ( let i = 1 ; i < rows . length ; i ++ ) {
1173+ state . write ( "\n" ) ;
1174+ state . write ( tableRowToMarkdown ( rows [ i ] ) ) ;
1175+ }
1176+
1177+ state . closeBlock ( node ) ;
1178+ } ,
1179+
1180+ tableRow ( ) { } ,
1181+
1182+ tableCell ( ) { } ,
1183+
1184+ tableHeader ( ) { } ,
1185+
10131186 taskList ( state , node ) {
10141187 state . renderList ( node , " " , ( ) => "- " ) ;
10151188 } ,
0 commit comments