Skip to content

Commit 8932d81

Browse files
authored
fix(table): make the parsing of cell class stricter (#444)
1 parent 7133f64 commit 8932d81

File tree

5 files changed

+146
-30
lines changed

5 files changed

+146
-30
lines changed

src/transform/plugins/table/index.ts

+15-28
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import StateBlock from 'markdown-it/lib/rules_block/state_block';
22
import {MarkdownItPluginCb} from '../typings';
33
import Token from 'markdown-it/lib/token';
4+
import {parseAttrsClass} from './utils';
45

56
const pluginName = 'yfm_table';
67
const pipeChar = 0x7c; // |
@@ -216,28 +217,6 @@ function getTableRowPositions(
216217
return {rows, endOfTable};
217218
}
218219

219-
/**
220-
* Removes the specified attribute from attributes in the content of a token.
221-
*
222-
* @param {Token} contentToken - The target token.
223-
* @param {string} attr - The attribute to be removed from the token content.
224-
*
225-
* @return {void}
226-
*/
227-
function removeAttrFromTokenContent(contentToken: Token, attr: string): void {
228-
// Replace the attribute in the token content with an empty string.
229-
const blockRegex = /\s*\{[^}]*}/;
230-
const allAttrs = contentToken.content.match(blockRegex);
231-
if (!allAttrs) {
232-
return;
233-
}
234-
let replacedContent = allAttrs[0].replace(`.${attr}`, '');
235-
if (replacedContent.trim() === '{}') {
236-
replacedContent = '';
237-
}
238-
contentToken.content = contentToken.content.replace(allAttrs[0], replacedContent);
239-
}
240-
241220
/**
242221
* Extracts the class attribute from the given content token and applies it to the tdOpenToken.
243222
* Preserves other attributes.
@@ -248,12 +227,20 @@ function removeAttrFromTokenContent(contentToken: Token, attr: string): void {
248227
*/
249228
function extractAndApplyClassFromToken(contentToken: Token, tdOpenToken: Token): void {
250229
// Regex to find class attribute in any position within brackets
251-
const classAttrRegex = /(?<=\{[^}]*)\.([-_a-zA-Z0-9]+)/g;
252-
const classAttrMatch = classAttrRegex.exec(contentToken.content);
253-
if (classAttrMatch) {
254-
const classAttr = classAttrMatch[1];
255-
tdOpenToken.attrSet('class', classAttr);
256-
removeAttrFromTokenContent(contentToken, classAttr);
230+
const blockRegex = /\s*\{[^}]*}$/;
231+
const allAttrs = contentToken.content.match(blockRegex);
232+
if (!allAttrs) {
233+
return;
234+
}
235+
const attrsClass = parseAttrsClass(allAttrs[0].trim());
236+
if (attrsClass) {
237+
tdOpenToken.attrSet('class', attrsClass);
238+
// remove the class from the token so that it's not propagated to tr or table level
239+
let replacedContent = allAttrs[0].replace(`.${attrsClass}`, '');
240+
if (replacedContent.trim() === '{}') {
241+
replacedContent = '';
242+
}
243+
contentToken.content = contentToken.content.replace(allAttrs[0], replacedContent);
257244
}
258245
}
259246

src/transform/plugins/table/utils.ts

+44
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
/**
2+
* Parse the markdown-attrs format to retrieve a class name
3+
* Putting all the requirements in regex was more complicated than parsing a string char by char.
4+
*
5+
* @param {string} inputString - The string to parse.
6+
* @returns {string|null} - The extracted class or null if there is none
7+
*/
8+
9+
export function parseAttrsClass(inputString: string): string | null {
10+
const validChars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ .=-_';
11+
12+
if (!inputString.startsWith('{')) {
13+
return null;
14+
}
15+
16+
for (let i = 1; i < inputString.length; i++) {
17+
const char = inputString[i];
18+
19+
if (char === '}') {
20+
const contentInside = inputString.slice(1, i).trim(); // content excluding { and }
21+
22+
if (!contentInside) {
23+
return null;
24+
}
25+
26+
const parts = contentInside.split('.');
27+
if (parts.length !== 2 || !parts[1]) {
28+
return null;
29+
}
30+
//There should be a preceding whitespace
31+
if (!parts[0].endsWith(' ') && parts[0] !== '') {
32+
return null;
33+
}
34+
35+
return parts[1];
36+
}
37+
38+
if (!validChars.includes(char)) {
39+
return null;
40+
}
41+
}
42+
43+
return null;
44+
}

test/table.test.ts test/table/table.test.ts

+58-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
import transform from '../src/transform';
2-
import table from '../src/transform/plugins/table';
1+
import transform from '../../src/transform';
2+
import table from '../../src/transform/plugins/table';
3+
import includes from '../../src/transform/plugins/includes';
34

45
const transformYfm = (text: string) => {
56
const {
@@ -1259,3 +1260,58 @@ describe('Table plugin', () => {
12591260
});
12601261
});
12611262
});
1263+
1264+
const mocksPath = require.resolve('../utils.ts');
1265+
1266+
const transformWithIncludes = (text: string) => {
1267+
const {
1268+
result: {html},
1269+
} = transform(text, {
1270+
plugins: [table, includes],
1271+
path: mocksPath,
1272+
});
1273+
return html;
1274+
};
1275+
1276+
describe('table with includes', () => {
1277+
it('should preserve include paths', () => {
1278+
expect(
1279+
transformWithIncludes(
1280+
'#|\n' +
1281+
'|| **Table people** | **Table social_card** ||\n' +
1282+
'||\n' +
1283+
'\n' +
1284+
'\n' +
1285+
'{% include [create-folder](./mocks/include.md) %}\n' +
1286+
'\n' +
1287+
'|\n' +
1288+
'\n' +
1289+
'{% include [create-folder](./mocks/include.md) %}\n' +
1290+
'\n' +
1291+
'||\n' +
1292+
'|#',
1293+
),
1294+
).toEqual(
1295+
'<table>\n' +
1296+
'<tbody>\n' +
1297+
'<tr>\n' +
1298+
'<td>\n' +
1299+
'<p><strong>Table people</strong></p>\n' +
1300+
'</td>\n' +
1301+
'<td>\n' +
1302+
'<p><strong>Table social_card</strong></p>\n' +
1303+
'</td>\n' +
1304+
'</tr>\n' +
1305+
'<tr>\n' +
1306+
'<td>\n' +
1307+
'<p>{% include <a href="./mocks/include.md">create-folder</a> %}</p>\n' +
1308+
'</td>\n' +
1309+
'<td>\n' +
1310+
'<p>{% include <a href="./mocks/include.md">create-folder</a> %}</p>\n' +
1311+
'</td>\n' +
1312+
'</tr>\n' +
1313+
'</tbody>\n' +
1314+
'</table>\n',
1315+
);
1316+
});
1317+
});

test/table/utils.test.ts

+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import {parseAttrsClass} from '../../src/transform/plugins/table/utils';
2+
3+
describe('parseAttrsClass', () => {
4+
it('should correctly parse a class in markdown attrs format', () => {
5+
expect(parseAttrsClass('{property=value .class}')).toEqual('class');
6+
});
7+
8+
it('should correctly parse a class when its the only property', () => {
9+
expect(parseAttrsClass('{.class}')).toEqual('class');
10+
});
11+
12+
it('should require a whitespace if there are other properties', () => {
13+
expect(parseAttrsClass('{property=value.class}')).toEqual(null);
14+
});
15+
16+
it('should bail if there are unexpected symbols', () => {
17+
expect(parseAttrsClass('{property="value" .class}')).toEqual(null);
18+
});
19+
20+
it('should allow a dash in the class name', () => {
21+
expect(parseAttrsClass('{.cell-align-center}')).toEqual('cell-align-center');
22+
});
23+
24+
it('should not touch includes', () => {
25+
expect(
26+
parseAttrsClass('{% include <a href="./mocks/include.md">create-folder</a> %}'),
27+
).toEqual(null);
28+
});
29+
});

0 commit comments

Comments
 (0)