Skip to content

Commit

Permalink
refactor: Vitepress codeInContextPlugin (#1668)
Browse files Browse the repository at this point in the history
  • Loading branch information
Torres-ssf committed Jan 26, 2024
1 parent 466fbef commit 2aed04d
Show file tree
Hide file tree
Showing 3 changed files with 321 additions and 37 deletions.
2 changes: 2 additions & 0 deletions .changeset/thick-sheep-sneeze.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
---
---
256 changes: 256 additions & 0 deletions apps/docs/.vitepress/plugins/codeInContextPlugin.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,256 @@
import {
codeInContextPlugin,
isCodeSnippetToken,
createAnchorToken,
shouldAddDivider,
createDividerToken,
CodeSnippetToken,
} from './codeInContextPlugin';
import MarkdownIt from 'markdown-it';
import Token from 'markdown-it/lib/token';

describe('codeInContextPlugin', () => {
describe('isCodeSnippetToken', () => {
it('should returns true for valid code snippet tokens', () => {
const token = {
type: 'fence',
tag: 'code',
url: 'http://example.com',
} as unknown as Token;

expect(isCodeSnippetToken(token)).toBeTruthy();
});

it('should returns false for tokens without a url', () => {
const token: Token = { type: 'fence', tag: 'code' } as Token;
expect(isCodeSnippetToken(token)).toBeFalsy();
});

it('should returns false for tokens with a different type', () => {
const token = {
type: 'paragraph',
tag: 'code',
url: 'http://example.com',
} as unknown as Token;
expect(isCodeSnippetToken(token)).toBeFalsy();
});

it('should returns false for tokens with a different tag', () => {
const token = {
type: 'fence',
tag: 'div',
url: 'http://example.com',
} as unknown as Token;
expect(isCodeSnippetToken(token)).toBeFalsy();
});
});

describe('createAnchorToken', () => {
it('should creates an anchor token with the correct URL', () => {
const url = 'http://example.com';
const token = createAnchorToken(url);
expect(token.content).toBe(
`<a class="anchor-link" href="${url}" target="_blank" rel="noreferrer">See code in context</a>`
);
});

it('should creates an anchor token with an empty URL', () => {
const url = '';
const token = createAnchorToken(url);
expect(token.content).toBe(
`<a class="anchor-link" href="${url}" target="_blank" rel="noreferrer">See code in context</a>`
);
});
});

describe('shouldAddDivider', () => {
it('should returns false if it is the last token', () => {
const tokens: Token[] = [
{ type: 'fence', tag: 'code', url: 'http://example.com' } as unknown as Token,
];
const index = tokens.length - 1;
expect(shouldAddDivider(tokens, index)).toBeFalsy();
});

it('should returns true if the next token is not a header', () => {
const tokens: Token[] = [
{ type: 'fence', tag: 'code', url: 'http://example.com' } as unknown as Token,
{ type: 'paragraph' } as unknown as Token,
];
expect(shouldAddDivider(tokens, 0)).toBeTruthy();
});

it('should returns false if the next token is a header', () => {
const tokens: Token[] = [
{ type: 'fence', tag: 'code', url: 'http://example.com' } as unknown as Token,
{ type: 'heading', tag: 'h2' } as unknown as Token,
];
expect(shouldAddDivider(tokens, 0)).toBeFalsy();
});
});

describe('createDividerToken', () => {
it('should creates a divider token', () => {
const token = createDividerToken();
expect(token.type).toBe('hr');
expect(token.markup).toBe('---');
});
});

describe('codeInContextPlugin', () => {
function mockTokens(mockedTokens: Token[]): Token[] {
const md = new MarkdownIt();
const processedTokens: Token[] = [];

const mockTokensPlugin = (md: MarkdownIt) => {
// Mocking tokens that are going to be processed by the 'codeInContextPlugin'
md.core.ruler.before('add-anchor-link', 'mock-tokens', (state) => {
state.tokens = mockedTokens;

return state.tokens;
});
};

const extractUpdatedTokensPlugin = (md: MarkdownIt) => {
// Capturing tokens after they are processed by the 'codeInContextPlugin'
md.core.ruler.after('add-anchor-link', 'capture-updated-tokens', (state) => {
processedTokens.push(...state.tokens);
return state.tokens;
});
};

md.use(codeInContextPlugin);
md.use(mockTokensPlugin);
md.use(extractUpdatedTokensPlugin);

// Triggering the MarkdownIt processing
md.render('');

return processedTokens;
}

it('should add an token link after code snippet with the URL', () => {
const url = 'http://example.com';
const mockedTokens = [
{
type: 'fence',
tag: 'code',
url,
content: 'code snippet 1',
} as CodeSnippetToken,
];

const processedTokens = mockTokens(mockedTokens);

const content = `<a class="anchor-link" href="${url}" target="_blank" rel="noreferrer">See code in context</a>`;
expect(processedTokens).toContainEqual(
expect.objectContaining({ type: 'html_inline', content })
);
expect(processedTokens.length).toBe(mockedTokens.length + 1); // Token for the anchor link was added
});

it('should NOT add token link for code snippet without URL', () => {
const mockedTokens = [
{
type: 'fence',
tag: 'code',
content: 'code snippet 1',
} as CodeSnippetToken,
];

const processedTokens = mockTokens(mockedTokens);

expect(processedTokens).not.toContainEqual(expect.objectContaining({ type: 'html_inline' }));
expect(processedTokens.length).toBe(mockedTokens.length); // No token for the anchor link was added
});

it('should NOT add anchor link for non code snippet tokens', () => {
Array.from({ length: 2 }).forEach((_, index) => {
const mockedTokens = [
{
type: index === 0 ? 'fence' : 'paagraph',
tag: index === 1 ? 'code' : 'p',
content: 'code snippet 1',
url: 'http://example.com',
} as CodeSnippetToken,
];

const processedTokens = mockTokens(mockedTokens);

expect(processedTokens).not.toContainEqual(
expect.objectContaining({ type: 'html_inline' })
);
expect(processedTokens.length).toBe(mockedTokens.length); // No token for the anchor link was added
});
});

it('should add a "hr" divider when code snippet is not last token', () => {
const mockedTokens = [
{
type: 'fence',
tag: 'code',
content: 'code snippet 1',
url: 'http://example.com',
} as CodeSnippetToken,
{
type: 'text',
tag: '',
content: 'Hello World',
} as Token,
];

const processedTokens = mockTokens(mockedTokens);

expect(processedTokens).toContainEqual(
expect.objectContaining({ markup: '---', type: 'hr' })
);
expect(processedTokens.length).toBe(mockedTokens.length + 2); // Link and divider Tokens were added
});

it('should NOT add a "hr" divider when code snippet is last token', () => {
const mockedTokens = [
{
type: 'text',
tag: '',
content: 'Hello World',
} as Token,
{
type: 'fence',
tag: 'code',
content: 'code snippet 1',
url: 'http://example.com',
} as CodeSnippetToken,
];

const processedTokens = mockTokens(mockedTokens);

expect(processedTokens).not.toContainEqual(
expect.objectContaining({ markup: '---', type: 'hr' })
);
expect(processedTokens.length).toBe(mockedTokens.length + 1); // Only Token link was added
});

it('should NOT add a "hr" divider when next token is "h2"', () => {
const mockedTokens = [
{
type: 'fence',
tag: 'code',
content: 'code snippet 1',
url: 'http://example.com',
} as CodeSnippetToken,
{
type: 'text',
tag: 'h2',
content: 'Hello World',
} as Token,
];

const processedTokens = mockTokens(mockedTokens);

expect(processedTokens).not.toContainEqual(
expect.objectContaining({ markup: '---', type: 'hr' })
);
expect(processedTokens.length).toBe(mockedTokens.length + 1); // Only Token link was added
});
});
});
100 changes: 63 additions & 37 deletions apps/docs/.vitepress/plugins/codeInContextPlugin.ts
Original file line number Diff line number Diff line change
@@ -1,51 +1,77 @@
import MarkdownIt from 'markdown-it';
import Token from 'markdown-it/lib/token';

type CustomTokem = Token & { url?: string };
export interface CodeSnippetToken extends Token {
type: 'fence';
tag: 'code';
url: string;
}

export function codeInContextPlugin(md: MarkdownIt) {
/**
* Adds anchor link 'code in context' to all for all CodeSnippetTokens.
* @param md - The MarkdownIt instance.
*/
export const codeInContextPlugin = (md: MarkdownIt) => {
md.core.ruler.after('inline', 'add-anchor-link', (state) => {
let newTokens: CustomTokem[] = [];
const newTokens: Token[] = [];

state.tokens.forEach((token: CustomTokem, index) => {
state.tokens.forEach((token, index) => {
newTokens.push(token);

if (token.type === 'fence' && token.tag === 'code' && token.url) {
const { url } = token;

/**
* Extracting 'url' prop inserted by "snippetPlugin" and creating
* "See code in context" link after code snippet.
*/
const anchorToken = new Token('html_inline', '', 0);
anchorToken.content = `<a class="anchor-link"
href="${url}"
target="_blank"
rel="noreferrer">See code in context
</a>`;

newTokens.push(anchorToken);

let shouldAddDivider = true;

if (index + 1 >= state.tokens.length) {
shouldAddDivider = false;
} else {
const nextToken = state.tokens[index + 1];

if (nextToken && /h2/.test(nextToken.tag)) {
shouldAddDivider = false;
}
}

if (shouldAddDivider) {
const divisorToken = new Token('hr', 'hr', 0);
divisorToken.markup = '---';
newTokens.push(divisorToken);
if (isCodeSnippetToken(token)) {
newTokens.push(createAnchorToken(token.url));
if (shouldAddDivider(state.tokens, index)) {
newTokens.push(createDividerToken());
}
}
});

state.tokens = newTokens;
});
}
};

/**
* Checks if a given token is a CodeSnippetToken.
* @param token The token to check.
* @returns True if the token is a CodeSnippetToken, false otherwise.
*/
export const isCodeSnippetToken = (token: Token): token is CodeSnippetToken => {
return token.type === 'fence' && token.tag === 'code' && 'url' in token;
};

/**
* Creates an anchor link token with the specified URL.
*
* @param url - The URL to be used in the anchor tag.
* @returns The created anchor token.
*/
export const createAnchorToken = (url: string): Token => {
const anchorToken = new Token('html_inline', '', 0);
anchorToken.content = `<a class="anchor-link" href="${url}" target="_blank" rel="noreferrer">See code in context</a>`;
return anchorToken;
};

/**
* Determines whether a divider should be added after a code snippet.
* @param tokens - The array of tokens.
* @param index - The index of the current token.
* @returns True if a divider should be added, false otherwise.
*/
export const shouldAddDivider = (tokens: Token[], index: number): boolean => {
/**
* The divider should be added only if a next token exists and if it is not
* a 'h2' tag, since an 'hr' tag is already added before this tag by default.
*/
const nextToken = tokens[index + 1];
return !!nextToken && !/h2/.test(nextToken.tag);
};

/**
* Creates a divider token.
* @returns The created divider token.
*/
export const createDividerToken = (): Token => {
const divisorToken = new Token('hr', 'hr', 0);
divisorToken.markup = '---';
return divisorToken;
};

0 comments on commit 2aed04d

Please sign in to comment.