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

refactor: Vitepress codeInContextPlugin #1668

Merged
merged 8 commits into from
Jan 26, 2024
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;
};
Loading