Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(table): make the parsing of cell class stricter #444

Merged
merged 2 commits into from
Jun 17, 2024
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
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;
}
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);
});
});
Loading