-
-
Notifications
You must be signed in to change notification settings - Fork 6.5k
chore(shiki): only send required langs to client, and support more on server #7787
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
Merged
Merged
Changes from all commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
da768d0
chore(shiki): add languages used in core
avivkeller 4bc3636
chore(shiki): only send required langs to client
avivkeller 199ff6e
fixup! chore(shiki): only send required langs to client
avivkeller c7a9aa4
fix unit tests
avivkeller 056fc43
fixup! fix unit tests
avivkeller cb04de4
Update packages/rehype-shiki/src/highlighter.mjs
avivkeller ca998e8
Update packages/rehype-shiki/src/highlighter.mjs
avivkeller File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or 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
This file contains hidden or 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
This file contains hidden or 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
This file contains hidden or 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,75 @@ | ||
| import assert from 'node:assert/strict'; | ||
| import { describe, it, mock } from 'node:test'; | ||
|
|
||
| // Mock dependencies | ||
| const mockShiki = { | ||
| codeToHtml: mock.fn(() => '<pre><code>highlighted code</code></pre>'), | ||
| codeToHast: mock.fn(() => ({ type: 'element', tagName: 'pre' })), | ||
| }; | ||
|
|
||
| mock.module('@shikijs/core', { | ||
| namedExports: { createHighlighterCoreSync: () => mockShiki }, | ||
| }); | ||
|
|
||
| mock.module('@shikijs/engine-javascript', { | ||
| namedExports: { createJavaScriptRegexEngine: () => ({}) }, | ||
| }); | ||
|
|
||
| mock.module('shiki/themes/nord.mjs', { | ||
| defaultExport: { name: 'nord', colors: { 'editor.background': '#2e3440' } }, | ||
| }); | ||
|
|
||
| describe('createHighlighter', async () => { | ||
| const { createHighlighter } = await import('../highlighter.mjs'); | ||
|
|
||
| describe('getLanguageDisplayName', () => { | ||
| it('returns display name for known languages', () => { | ||
| const langs = [ | ||
| { name: 'javascript', displayName: 'JavaScript', aliases: ['js'] }, | ||
| ]; | ||
| const highlighter = createHighlighter({ langs }); | ||
|
|
||
| assert.strictEqual( | ||
| highlighter.getLanguageDisplayName('javascript'), | ||
| 'JavaScript' | ||
| ); | ||
| assert.strictEqual( | ||
| highlighter.getLanguageDisplayName('js'), | ||
| 'JavaScript' | ||
| ); | ||
| }); | ||
|
|
||
| it('returns original name for unknown languages', () => { | ||
| const highlighter = createHighlighter({ langs: [] }); | ||
| assert.strictEqual( | ||
| highlighter.getLanguageDisplayName('unknown'), | ||
| 'unknown' | ||
| ); | ||
| }); | ||
| }); | ||
|
|
||
| describe('highlightToHtml', () => { | ||
| it('extracts inner HTML from code tag', () => { | ||
| mockShiki.codeToHtml.mock.mockImplementationOnce( | ||
| () => '<pre><code>const x = 1;</code></pre>' | ||
| ); | ||
|
|
||
| const highlighter = createHighlighter({}); | ||
| const result = highlighter.highlightToHtml('const x = 1;', 'javascript'); | ||
|
|
||
| assert.strictEqual(result, 'const x = 1;'); | ||
| }); | ||
| }); | ||
|
|
||
| describe('highlightToHast', () => { | ||
| it('returns HAST tree from shiki', () => { | ||
| const expectedHast = { type: 'element', tagName: 'pre' }; | ||
| mockShiki.codeToHast.mock.mockImplementationOnce(() => expectedHast); | ||
|
|
||
| const highlighter = createHighlighter({}); | ||
| const result = highlighter.highlightToHast('code', 'javascript'); | ||
|
|
||
| assert.deepStrictEqual(result, expectedHast); | ||
| }); | ||
| }); | ||
| }); |
This file was deleted.
Oops, something went wrong.
This file contains hidden or 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,67 @@ | ||
| import assert from 'node:assert/strict'; | ||
| import { describe, it, mock } from 'node:test'; | ||
|
|
||
| // Simplified mocks - only mock what's actually needed | ||
| mock.module('../index.mjs', { | ||
| namedExports: { highlightToHast: mock.fn(() => ({ children: [] })) }, | ||
| }); | ||
|
|
||
| mock.module('classnames', { | ||
| defaultExport: (...args) => args.filter(Boolean).join(' '), | ||
| }); | ||
|
|
||
| mock.module('hast-util-to-string', { | ||
| namedExports: { toString: () => 'code' }, | ||
| }); | ||
|
|
||
| const mockVisit = mock.fn(); | ||
| mock.module('unist-util-visit', { | ||
| namedExports: { visit: mockVisit, SKIP: Symbol() }, | ||
| }); | ||
|
|
||
| describe('rehypeShikiji', async () => { | ||
| const { default: rehypeShikiji } = await import('../plugin.mjs'); | ||
| const mockTree = { type: 'root', children: [] }; | ||
|
|
||
| it('calls visit twice', () => { | ||
| mockVisit.mock.resetCalls(); | ||
| rehypeShikiji()(mockTree); | ||
| assert.strictEqual(mockVisit.mock.calls.length, 2); | ||
| }); | ||
|
|
||
| it('creates CodeTabs for multiple code blocks', () => { | ||
| const parent = { | ||
| children: [ | ||
| { | ||
| tagName: 'pre', | ||
| children: [ | ||
| { | ||
| tagName: 'code', | ||
| data: { meta: 'displayName="JS"' }, | ||
| properties: { className: ['language-js'] }, | ||
| }, | ||
| ], | ||
| }, | ||
| { | ||
| tagName: 'pre', | ||
| children: [ | ||
| { | ||
| tagName: 'code', | ||
| data: { meta: 'displayName="TS"' }, | ||
| properties: { className: ['language-ts'] }, | ||
| }, | ||
| ], | ||
| }, | ||
| ], | ||
| }; | ||
|
|
||
| mockVisit.mock.mockImplementation((tree, selector, visitor) => { | ||
| if (selector === 'element') { | ||
| visitor(parent.children[0], 0, parent); | ||
| } | ||
| }); | ||
|
|
||
| rehypeShikiji()(mockTree); | ||
| assert.ok(parent.children.some(child => child.tagName === 'CodeTabs')); | ||
| }); | ||
| }); |
This file contains hidden or 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,47 +1,67 @@ | ||
| import { createHighlighterCoreSync } from '@shikijs/core'; | ||
| import { createJavaScriptRegexEngine } from '@shikijs/engine-javascript'; | ||
| import shikiNordTheme from 'shiki/themes/nord.mjs'; | ||
|
|
||
| import { LANGUAGES, DEFAULT_THEME } from './languages.mjs'; | ||
|
|
||
| let _shiki; | ||
|
|
||
| /** | ||
| * Lazy-load and memoize the minimal Shikiji Syntax Highlighter | ||
| * @returns {import('@shikijs/core').HighlighterCore} | ||
| */ | ||
| export const getShiki = () => { | ||
| if (!_shiki) { | ||
| _shiki = createHighlighterCoreSync({ | ||
| themes: [DEFAULT_THEME], | ||
| langs: LANGUAGES, | ||
| // Let's use Shiki's new Experimental JavaScript-based regex engine! | ||
| engine: createJavaScriptRegexEngine(), | ||
| }); | ||
| } | ||
| return _shiki; | ||
| const DEFAULT_THEME = { | ||
| // We are updating this color because the background color and comment text color | ||
| // in the Codebox component do not comply with accessibility standards | ||
| // @see https://www.w3.org/WAI/WCAG21/Understanding/contrast-minimum.html | ||
| colorReplacements: { '#616e88': '#707e99' }, | ||
| ...shikiNordTheme, | ||
| }; | ||
|
|
||
| /** | ||
| * Highlights code and returns the inner HTML inside the <code> tag | ||
| * | ||
| * @param {string} code - The code to highlight | ||
| * @param {string} language - The programming language to use for highlighting | ||
| * @returns {string} The inner HTML of the highlighted code | ||
| * Creates a syntax highlighter with utility functions | ||
| * @param {import('@shikijs/core').HighlighterCoreOptions} options - Configuration options for the highlighter | ||
| */ | ||
| export const highlightToHtml = (code, language) => | ||
| getShiki() | ||
| .codeToHtml(code, { lang: language, theme: DEFAULT_THEME }) | ||
| // Shiki will always return the Highlighted code encapsulated in a <pre> and <code> tag | ||
| // since our own CodeBox component handles the <code> tag, we just want to extract | ||
| // the inner highlighted code to the CodeBox | ||
| .match(/<code>(.+?)<\/code>/s)[1]; | ||
| export const createHighlighter = options => { | ||
| const shiki = createHighlighterCoreSync({ | ||
| themes: [DEFAULT_THEME], | ||
| engine: createJavaScriptRegexEngine(), | ||
| ...options, | ||
| }); | ||
|
|
||
| /** | ||
| * Highlights code and returns a HAST tree | ||
| * | ||
| * @param {string} code - The code to highlight | ||
| * @param {string} language - The programming language to use for highlighting | ||
| * @returns {import('hast').Element} The HAST representation of the highlighted code | ||
| */ | ||
| export const highlightToHast = (code, language) => | ||
| getShiki().codeToHast(code, { lang: language, theme: DEFAULT_THEME }); | ||
| const theme = options.themes?.[0] ?? DEFAULT_THEME; | ||
| const langs = options.langs ?? []; | ||
|
|
||
| const getLanguageDisplayName = language => { | ||
| const languageByIdOrAlias = langs.find( | ||
| ({ name, aliases }) => | ||
| name.toLowerCase() === language.toLowerCase() || | ||
| (aliases !== undefined && aliases.includes(language.toLowerCase())) | ||
| ); | ||
|
|
||
| return languageByIdOrAlias?.displayName ?? language; | ||
| }; | ||
|
|
||
| /** | ||
| * Highlights code and returns the inner HTML inside the <code> tag | ||
| * | ||
| * @param {string} code - The code to highlight | ||
| * @param {string} language - The programming language to use for highlighting | ||
| * @returns {string} The inner HTML of the highlighted code | ||
| */ | ||
| const highlightToHtml = (code, language) => | ||
| shiki | ||
| .codeToHtml(code, { lang: language, theme }) | ||
| // Shiki will always return the Highlighted code encapsulated in a <pre> and <code> tag | ||
| // since our own CodeBox component handles the <code> tag, we just want to extract | ||
| // the inner highlighted code to the CodeBox | ||
| .match(/<code>(.+?)<\/code>/s)[1]; | ||
|
|
||
| /** | ||
| * Highlights code and returns a HAST tree | ||
| * | ||
| * @param {string} code - The code to highlight | ||
| * @param {string} language - The programming language to use for highlighting | ||
| */ | ||
| const highlightToHast = (code, language) => | ||
| shiki.codeToHast(code, { lang: language, theme }); | ||
|
|
||
| return { | ||
| shiki, | ||
| getLanguageDisplayName, | ||
| highlightToHtml, | ||
| highlightToHast, | ||
| }; | ||
| }; | ||
This file contains hidden or 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,5 +1,43 @@ | ||
| import { rehypeShikiji } from './plugin.mjs'; | ||
| export * from './highlighter.mjs'; | ||
| export * from './languages.mjs'; | ||
| import cLanguage from 'shiki/langs/c.mjs'; | ||
| import coffeeScriptLanguage from 'shiki/langs/coffeescript.mjs'; | ||
| import cPlusPlusLanguage from 'shiki/langs/cpp.mjs'; | ||
| import diffLanguage from 'shiki/langs/diff.mjs'; | ||
| import dockerLanguage from 'shiki/langs/docker.mjs'; | ||
| import httpLanguage from 'shiki/langs/http.mjs'; | ||
| import iniLanguage from 'shiki/langs/ini.mjs'; | ||
| import javaScriptLanguage from 'shiki/langs/javascript.mjs'; | ||
| import jsonLanguage from 'shiki/langs/json.mjs'; | ||
| import powershellLanguage from 'shiki/langs/powershell.mjs'; | ||
| import shellScriptLanguage from 'shiki/langs/shellscript.mjs'; | ||
| import shellSessionLanguage from 'shiki/langs/shellsession.mjs'; | ||
| import typeScriptLanguage from 'shiki/langs/typescript.mjs'; | ||
| import yamlLanguage from 'shiki/langs/yaml.mjs'; | ||
|
|
||
| export default rehypeShikiji; | ||
| import { createHighlighter } from './highlighter.mjs'; | ||
|
|
||
| const { shiki, getLanguageDisplayName, highlightToHast, highlightToHtml } = | ||
| createHighlighter({ | ||
| langs: [ | ||
| ...cLanguage, | ||
| ...coffeeScriptLanguage, | ||
| ...cPlusPlusLanguage, | ||
| ...diffLanguage, | ||
| ...dockerLanguage, | ||
| ...httpLanguage, | ||
| ...iniLanguage, | ||
| { | ||
| ...javaScriptLanguage[0], | ||
| // We patch the JavaScript language to include the CommonJS and ES Module aliases | ||
| // that are commonly used (non-standard aliases) within our API docs and Blog posts | ||
| aliases: javaScriptLanguage[0].aliases.concat('cjs', 'mjs'), | ||
| }, | ||
| ...jsonLanguage, | ||
| ...powershellLanguage, | ||
| ...shellScriptLanguage, | ||
| ...shellSessionLanguage, | ||
| ...typeScriptLanguage, | ||
| ...yamlLanguage, | ||
| ], | ||
| }); | ||
|
|
||
| export { shiki, getLanguageDisplayName, highlightToHast, highlightToHtml }; | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.