Skip to content

Commit

Permalink
feat: add custom attrs
Browse files Browse the repository at this point in the history
  • Loading branch information
v8tenko committed Jul 15, 2024
1 parent 5f35b15 commit 1c3bc13
Show file tree
Hide file tree
Showing 4 changed files with 169 additions and 31 deletions.
26 changes: 22 additions & 4 deletions src/transform/plugins/table/index.ts
Original file line number Diff line number Diff line change
@@ -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; // |
Expand Down Expand Up @@ -96,6 +96,7 @@ class StateIterator {
interface RowPositions {
rows: [number, number, [Stats, Stats][]][];
endOfTable: number | null;
pos: number;
}

function getTableRowPositions(
Expand Down Expand Up @@ -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<string, string[]> {
const attrsStringStart = state.skipSpaces(pos);
const attrsString = state.src.slice(attrsStringStart);

return parseAttrs(attrsString) ?? {};
}

/**
Expand All @@ -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
Expand Down Expand Up @@ -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;
Expand All @@ -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);
Expand Down
99 changes: 81 additions & 18 deletions src/transform/plugins/table/utils.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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<string, string[]> = {
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)) {
Expand Down
43 changes: 43 additions & 0 deletions test/table/table.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,49 @@ describe('Table plugin', () => {
'</table>\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(
'<table class="test name" id="id" data-diplodoc-large-table="true" data-wide-preview="true">\n' +
'<tbody>\n' +
'<tr>\n' +
'<td>\n' +
'<p>Cell in column 1, row 1</p>\n' +
'</td>\n' +
'<td>\n' +
'<p>Cell in column 2, row 1</p>\n' +
'</td>\n' +
'</tr>\n' +
'<tr>\n' +
'<td>\n' +
'<p>Cell in column 1, row 2</p>\n' +
'</td>\n' +
'<td>\n' +
'<p>Cell in column 2, row 2</p>\n' +
'</td>\n' +
'</tr>\n' +
'<tr>\n' +
'<td>\n' +
'<p>Cell in column 1, row 3</p>\n' +
'</td>\n' +
'<td>\n' +
'<p>Cell in column 2, row 3</p>\n' +
'</td>\n' +
'</tr>\n' +
'</tbody>\n' +
'</table>\n',
);
});
it('should render table between paragraphs', () => {
expect(
transformYfm(
Expand Down
32 changes: 23 additions & 9 deletions test/table/utils.test.ts
Original file line number Diff line number Diff line change
@@ -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 <a href="./mocks/include.md">create-folder</a> %}'),
).toEqual(null);
expect(parseAttrs('{% include <a href="./mocks/include.md">create-folder</a> %}')).toEqual(
null,
);
});
});

0 comments on commit 1c3bc13

Please sign in to comment.