Skip to content

Commit

Permalink
fix(table): make the parsing of cell class stricter (#444)
Browse files Browse the repository at this point in the history
  • Loading branch information
main-kun authored Jun 17, 2024
1 parent 7133f64 commit 8932d81
Show file tree
Hide file tree
Showing 5 changed files with 146 additions and 30 deletions.
43 changes: 15 additions & 28 deletions src/transform/plugins/table/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import StateBlock from 'markdown-it/lib/rules_block/state_block';
import {MarkdownItPluginCb} from '../typings';
import Token from 'markdown-it/lib/token';
import {parseAttrsClass} from './utils';

const pluginName = 'yfm_table';
const pipeChar = 0x7c; // |
Expand Down Expand Up @@ -216,28 +217,6 @@ function getTableRowPositions(
return {rows, endOfTable};
}

/**
* Removes the specified attribute from attributes in the content of a token.
*
* @param {Token} contentToken - The target token.
* @param {string} attr - The attribute to be removed from the token content.
*
* @return {void}
*/
function removeAttrFromTokenContent(contentToken: Token, attr: string): void {
// Replace the attribute in the token content with an empty string.
const blockRegex = /\s*\{[^}]*}/;
const allAttrs = contentToken.content.match(blockRegex);
if (!allAttrs) {
return;
}
let replacedContent = allAttrs[0].replace(`.${attr}`, '');
if (replacedContent.trim() === '{}') {
replacedContent = '';
}
contentToken.content = contentToken.content.replace(allAttrs[0], replacedContent);
}

/**
* Extracts the class attribute from the given content token and applies it to the tdOpenToken.
* Preserves other attributes.
Expand All @@ -248,12 +227,20 @@ function removeAttrFromTokenContent(contentToken: Token, attr: string): void {
*/
function extractAndApplyClassFromToken(contentToken: Token, tdOpenToken: Token): void {
// Regex to find class attribute in any position within brackets
const classAttrRegex = /(?<=\{[^}]*)\.([-_a-zA-Z0-9]+)/g;
const classAttrMatch = classAttrRegex.exec(contentToken.content);
if (classAttrMatch) {
const classAttr = classAttrMatch[1];
tdOpenToken.attrSet('class', classAttr);
removeAttrFromTokenContent(contentToken, classAttr);
const blockRegex = /\s*\{[^}]*}$/;
const allAttrs = contentToken.content.match(blockRegex);
if (!allAttrs) {
return;
}
const attrsClass = parseAttrsClass(allAttrs[0].trim());
if (attrsClass) {
tdOpenToken.attrSet('class', attrsClass);
// remove the class from the token so that it's not propagated to tr or table level
let replacedContent = allAttrs[0].replace(`.${attrsClass}`, '');
if (replacedContent.trim() === '{}') {
replacedContent = '';
}
contentToken.content = contentToken.content.replace(allAttrs[0], replacedContent);
}
}

Expand Down
44 changes: 44 additions & 0 deletions src/transform/plugins/table/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/**
* Parse the markdown-attrs format to retrieve a class name
* Putting all the requirements in regex was more complicated than parsing a string char by char.
*
* @param {string} inputString - The string to parse.
* @returns {string|null} - The extracted class or null if there is none
*/

export function parseAttrsClass(inputString: string): string | null {
const validChars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ .=-_';

if (!inputString.startsWith('{')) {
return null;
}

for (let i = 1; i < inputString.length; i++) {
const char = inputString[i];

if (char === '}') {
const contentInside = inputString.slice(1, i).trim(); // content excluding { and }

if (!contentInside) {
return null;
}

const parts = contentInside.split('.');
if (parts.length !== 2 || !parts[1]) {
return null;
}
//There should be a preceding whitespace
if (!parts[0].endsWith(' ') && parts[0] !== '') {
return null;
}

return parts[1];
}

if (!validChars.includes(char)) {
return null;
}
}

return null;
}
File renamed without changes.
60 changes: 58 additions & 2 deletions test/table.test.ts → test/table/table.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import transform from '../src/transform';
import table from '../src/transform/plugins/table';
import transform from '../../src/transform';
import table from '../../src/transform/plugins/table';
import includes from '../../src/transform/plugins/includes';

const transformYfm = (text: string) => {
const {
Expand Down Expand Up @@ -1259,3 +1260,58 @@ describe('Table plugin', () => {
});
});
});

const mocksPath = require.resolve('../utils.ts');

const transformWithIncludes = (text: string) => {
const {
result: {html},
} = transform(text, {
plugins: [table, includes],
path: mocksPath,
});
return html;
};

describe('table with includes', () => {
it('should preserve include paths', () => {
expect(
transformWithIncludes(
'#|\n' +
'|| **Table people** | **Table social_card** ||\n' +
'||\n' +
'\n' +
'\n' +
'{% include [create-folder](./mocks/include.md) %}\n' +
'\n' +
'|\n' +
'\n' +
'{% include [create-folder](./mocks/include.md) %}\n' +
'\n' +
'||\n' +
'|#',
),
).toEqual(
'<table>\n' +
'<tbody>\n' +
'<tr>\n' +
'<td>\n' +
'<p><strong>Table people</strong></p>\n' +
'</td>\n' +
'<td>\n' +
'<p><strong>Table social_card</strong></p>\n' +
'</td>\n' +
'</tr>\n' +
'<tr>\n' +
'<td>\n' +
'<p>{% include <a href="./mocks/include.md">create-folder</a> %}</p>\n' +
'</td>\n' +
'<td>\n' +
'<p>{% include <a href="./mocks/include.md">create-folder</a> %}</p>\n' +
'</td>\n' +
'</tr>\n' +
'</tbody>\n' +
'</table>\n',
);
});
});
29 changes: 29 additions & 0 deletions test/table/utils.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import {parseAttrsClass} from '../../src/transform/plugins/table/utils';

describe('parseAttrsClass', () => {
it('should correctly parse a class in markdown attrs format', () => {
expect(parseAttrsClass('{property=value .class}')).toEqual('class');
});

it('should correctly parse a class when its the only property', () => {
expect(parseAttrsClass('{.class}')).toEqual('class');
});

it('should require a whitespace if there are other properties', () => {
expect(parseAttrsClass('{property=value.class}')).toEqual(null);
});

it('should bail if there are unexpected symbols', () => {
expect(parseAttrsClass('{property="value" .class}')).toEqual(null);
});

it('should allow a dash in the class name', () => {
expect(parseAttrsClass('{.cell-align-center}')).toEqual('cell-align-center');
});

it('should not touch includes', () => {
expect(
parseAttrsClass('{% include <a href="./mocks/include.md">create-folder</a> %}'),
).toEqual(null);
});
});

0 comments on commit 8932d81

Please sign in to comment.