diff --git a/src/transform/plugins/table/index.ts b/src/transform/plugins/table/index.ts index 85f53b9c..361f8401 100644 --- a/src/transform/plugins/table/index.ts +++ b/src/transform/plugins/table/index.ts @@ -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; // | @@ -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. @@ -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); } } diff --git a/src/transform/plugins/table/utils.ts b/src/transform/plugins/table/utils.ts new file mode 100644 index 00000000..595cd2ac --- /dev/null +++ b/src/transform/plugins/table/utils.ts @@ -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; +} diff --git a/test/__snapshots__/table.test.ts.snap b/test/table/__snapshots__/table.test.ts.snap similarity index 100% rename from test/__snapshots__/table.test.ts.snap rename to test/table/__snapshots__/table.test.ts.snap diff --git a/test/table.test.ts b/test/table/table.test.ts similarity index 95% rename from test/table.test.ts rename to test/table/table.test.ts index f53bbee7..98fad3e4 100644 --- a/test/table.test.ts +++ b/test/table/table.test.ts @@ -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 { @@ -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( + '
\n' +
+ ' Table people \n' + + ' | \n' +
+ '\n' +
+ ' Table social_card \n' + + ' | \n' +
+ '
\n' +
+ ' {% include create-folder %} \n' + + ' | \n' +
+ '\n' +
+ ' {% include create-folder %} \n' + + ' | \n' +
+ '