diff --git a/src/transform/plugins/table/index.ts b/src/transform/plugins/table/index.ts index 361f8401..5e116ae8 100644 --- a/src/transform/plugins/table/index.ts +++ b/src/transform/plugins/table/index.ts @@ -1,7 +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'; +import {parseAttrs} from './utils'; const pluginName = 'yfm_table'; const pipeChar = 0x7c; // | @@ -96,6 +96,7 @@ class StateIterator { interface RowPositions { rows: [number, number, [Stats, Stats][]][]; endOfTable: number | null; + pos: number; } function getTableRowPositions( @@ -214,7 +215,17 @@ function getTableRowPositions( iter.next(); } - return {rows, endOfTable}; + + const {pos} = iter; + + return {rows, endOfTable, pos}; +} + +function extractAttributes(state: StateBlock, pos: number): Record { + const attrsStringStart = state.skipSpaces(pos); + const attrsString = state.src.slice(attrsStringStart); + + return parseAttrs(attrsString) ?? {}; } /** @@ -232,7 +243,7 @@ function extractAndApplyClassFromToken(contentToken: Token, tdOpenToken: Token): if (!allAttrs) { return; } - const attrsClass = parseAttrsClass(allAttrs[0].trim()); + const attrsClass = parseAttrs(allAttrs[0].trim())?.class.join(' '); if (attrsClass) { tdOpenToken.attrSet('class', attrsClass); // remove the class from the token so that it's not propagated to tr or table level @@ -363,13 +374,15 @@ const yfmTable: MarkdownItPluginCb = (md) => { return true; } - const {rows, endOfTable} = getTableRowPositions( + const {rows, endOfTable, pos} = getTableRowPositions( state, startPosition, endPosition, startLine, ); + const attrs = extractAttributes(state, pos); + if (!endOfTable) { token = state.push('__yfm_lint', '', 0); token.hidden = true; @@ -385,6 +398,11 @@ const yfmTable: MarkdownItPluginCb = (md) => { const tableStart = state.tokens.length; token = state.push('yfm_table_open', 'table', 1); + + for (const [property, values] of Object.entries(attrs)) { + token.attrJoin(property, values.join(' ')); + } + token.map = [startLine, endOfTable]; token = state.push('yfm_tbody_open', 'tbody', 1); diff --git a/src/transform/plugins/table/utils.ts b/src/transform/plugins/table/utils.ts index 595cd2ac..08ec3aee 100644 --- a/src/transform/plugins/table/utils.ts +++ b/src/transform/plugins/table/utils.ts @@ -1,13 +1,61 @@ -/** - * 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 - */ +type DatasetKey = `data-${string}`; +type Attrs = 'class' | 'id' | DatasetKey; -export function parseAttrsClass(inputString: string): string | null { - const validChars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ .=-_'; +type Selector = (value: string) => { + key: Attrs; + value: string; +} | null; + +const wrapToData = (key: string): DatasetKey => { + if (key.startsWith('data-')) { + return key as DatasetKey; + } + + return `data-${key}`; +}; + +const selectors = { + class(value: string) { + if (value.startsWith('.')) { + return { + key: 'class', + value: value.slice(1), + }; + } + + return null; + }, + id(value: string) { + if (value.startsWith('#')) { + return { + key: 'id', + value: value.slice(1), + }; + } + + return null; + }, + attr(value: string) { + const parts = value.split('='); + + if (parts.length === 2) { + return { + key: wrapToData(parts[0]) as DatasetKey, + value: parts[1], + }; + } + + return { + key: wrapToData(value) as DatasetKey, + value: 'true', + }; + }, +}; + +const handlers = Object.values(selectors) as Selector[]; + +export function parseAttrs(inputString: string) { + const validChars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ .=-_#'; if (!inputString.startsWith('{')) { return null; @@ -23,16 +71,31 @@ export function parseAttrsClass(inputString: string): string | null { 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; - } + const parts = contentInside.split(' '); + + const attrs: Record = { + class: [], + id: [], + }; + + parts.forEach((part) => { + const matched = handlers.find((test) => test(part)); + + if (!matched) { + return; + } + + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const {key, value} = matched(part)!; + + if (!attrs[key]) { + attrs[key] = []; + } + + attrs[key].push(value); + }); - return parts[1]; + return attrs; } if (!validChars.includes(char)) { diff --git a/test/table/table.test.ts b/test/table/table.test.ts index 98fad3e4..cb04ee8a 100644 --- a/test/table/table.test.ts +++ b/test/table/table.test.ts @@ -79,6 +79,49 @@ describe('Table plugin', () => { '\n', ); }); + it('should render simple table', () => { + expect( + transformYfm( + '#|\n' + + '||Cell in column 1, row 1\n' + + '|Cell in column 2, row 1||\n' + + '||Cell in column 1, row 2\n' + + '|Cell in column 2, row 2||\n' + + '||Cell in column 1, row 3\n' + + '|Cell in column 2, row 3||\n' + + '|# {data-diplodoc-large-table=true .test .name #id wide-preview}', + ), + ).toBe( + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '
\n' + + '

Cell in column 1, row 1

\n' + + '
\n' + + '

Cell in column 2, row 1

\n' + + '
\n' + + '

Cell in column 1, row 2

\n' + + '
\n' + + '

Cell in column 2, row 2

\n' + + '
\n' + + '

Cell in column 1, row 3

\n' + + '
\n' + + '

Cell in column 2, row 3

\n' + + '
\n', + ); + }); it('should render table between paragraphs', () => { expect( transformYfm( diff --git a/test/table/utils.test.ts b/test/table/utils.test.ts index 8e046ba1..64d777b4 100644 --- a/test/table/utils.test.ts +++ b/test/table/utils.test.ts @@ -1,29 +1,43 @@ -import {parseAttrsClass} from '../../src/transform/plugins/table/utils'; +import {parseAttrs} 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'); + expect(parseAttrs('{property=value .class}')).toEqual({ + 'data-property': ['value'], + class: ['class'], + id: [], + }); }); it('should correctly parse a class when its the only property', () => { - expect(parseAttrsClass('{.class}')).toEqual('class'); + expect(parseAttrs('{.class}')).toEqual({ + class: ['class'], + id: [], + }); }); it('should require a whitespace if there are other properties', () => { - expect(parseAttrsClass('{property=value.class}')).toEqual(null); + expect(parseAttrs('{property=value.class}')).toEqual({ + 'data-property': ['value.class'], + id: [], + class: [], + }); }); it('should bail if there are unexpected symbols', () => { - expect(parseAttrsClass('{property="value" .class}')).toEqual(null); + expect(parseAttrs('{property="value" .class}')).toEqual(null); }); it('should allow a dash in the class name', () => { - expect(parseAttrsClass('{.cell-align-center}')).toEqual('cell-align-center'); + expect(parseAttrs('{.cell-align-center}')).toEqual({ + id: [], + class: ['cell-align-center'], + }); }); it('should not touch includes', () => { - expect( - parseAttrsClass('{% include create-folder %}'), - ).toEqual(null); + expect(parseAttrs('{% include create-folder %}')).toEqual( + null, + ); }); });