-
Notifications
You must be signed in to change notification settings - Fork 1.4k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
refactor: Vitepress
codeInContextPlugin
(#1668)
- Loading branch information
1 parent
466fbef
commit 2aed04d
Showing
3 changed files
with
321 additions
and
37 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
--- | ||
--- |
256 changes: 256 additions & 0 deletions
256
apps/docs/.vitepress/plugins/codeInContextPlugin.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
}; |