/** * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ import { getLineNumbersStart, type MagicCommentConfig, parseCodeBlockTitle, parseClassNameLanguage, parseLines, createCodeBlockMetadata, } from '../codeBlockUtils'; const defaultMagicComments: MagicCommentConfig[] = [ { className: 'theme-code-block-highlighted-line', line: 'highlight-next-line', block: {start: 'highlight-start', end: 'highlight-end'}, }, ]; describe('parseCodeBlockTitle', () => { it('parses double quote delimited title', () => { expect(parseCodeBlockTitle(`title="index.js"`)).toBe(`index.js`); }); it('parses single quote delimited title', () => { expect(parseCodeBlockTitle(`title='index.js'`)).toBe(`index.js`); }); it('does not parse mismatched quote delimiters', () => { expect(parseCodeBlockTitle(`title="index.js'`)).toBe(``); }); it('parses undefined metastring', () => { expect(parseCodeBlockTitle(undefined)).toBe(``); }); it('parses metastring with no title specified', () => { expect(parseCodeBlockTitle(`{1,2-3}`)).toBe(``); }); it('parses with multiple metadata title first', () => { expect(parseCodeBlockTitle(`title="index.js" label="JavaScript"`)).toBe( `index.js`, ); }); it('parses with multiple metadata title last', () => { expect(parseCodeBlockTitle(`label="JavaScript" title="index.js"`)).toBe( `index.js`, ); }); it('parses double quotes when delimited by single quotes', () => { expect(parseCodeBlockTitle(`title='console.log("Hello, World!")'`)).toBe( `console.log("Hello, World!")`, ); }); it('parses single quotes when delimited by double quotes', () => { expect(parseCodeBlockTitle(`title="console.log('Hello, World!')"`)).toBe( `console.log('Hello, World!')`, ); }); }); describe('parseClassNameLanguage', () => { it('works', () => { expect(parseClassNameLanguage('language-foo xxx yyy')).toBe('foo'); expect(parseClassNameLanguage('xxxxx language-foo yyy')).toBe('foo'); expect(parseClassNameLanguage('xx-language-foo yyyy')).toBeUndefined(); expect(parseClassNameLanguage('xxx yyy zzz')).toBeUndefined(); }); }); describe('parseLines', () => { it('does not parse content with metastring', () => { expect( parseLines('aaaaa\nnnnnn', { metastring: '{1}', language: 'js', magicComments: defaultMagicComments, }), ).toMatchInlineSnapshot(` { "code": "aaaaa nnnnn", "lineClassNames": { "0": [ "theme-code-block-highlighted-line", ], }, } `); expect( parseLines( `// highlight-next-line aaaaa bbbbb`, { metastring: '{1}', language: 'js', magicComments: defaultMagicComments, }, ), ).toMatchInlineSnapshot(` { "code": "// highlight-next-line aaaaa bbbbb", "lineClassNames": { "0": [ "theme-code-block-highlighted-line", ], }, } `); expect( parseLines( `aaaaa bbbbb`, { metastring: '{1}', language: 'undefined', magicComments: defaultMagicComments, }, ), ).toMatchInlineSnapshot(` { "code": "aaaaa bbbbb", "lineClassNames": { "0": [ "theme-code-block-highlighted-line", ], }, } `); expect(() => parseLines( `aaaaa bbbbb`, { metastring: '{1}', language: 'js', magicComments: [], }, ), ).toThrowErrorMatchingInlineSnapshot( `"A highlight range has been given in code block's metastring (\`\`\` {1}), but no magic comment config is available. Docusaurus applies the first magic comment entry's className for metastring ranges."`, ); }); it('does not parse content with no language', () => { expect( parseLines( `// highlight-next-line aaaaa bbbbb`, { metastring: '', language: undefined, magicComments: defaultMagicComments, }, ), ).toMatchInlineSnapshot(` { "code": "// highlight-next-line aaaaa bbbbb", "lineClassNames": {}, } `); }); it('removes lines correctly', () => { expect( parseLines( `// highlight-next-line aaaaa bbbbb`, {metastring: '', language: 'js', magicComments: defaultMagicComments}, ), ).toMatchInlineSnapshot(` { "code": "aaaaa bbbbb", "lineClassNames": { "0": [ "theme-code-block-highlighted-line", ], }, } `); expect( parseLines( `// highlight-start aaaaa // highlight-end bbbbb`, {metastring: '', language: 'js', magicComments: defaultMagicComments}, ), ).toMatchInlineSnapshot(` { "code": "aaaaa bbbbb", "lineClassNames": { "0": [ "theme-code-block-highlighted-line", ], }, } `); expect( parseLines( `// highlight-start // highlight-next-line aaaaa bbbbbbb // highlight-next-line // highlight-end bbbbb`, {metastring: '', language: 'js', magicComments: defaultMagicComments}, ), ).toMatchInlineSnapshot(` { "code": "aaaaa bbbbbbb bbbbb", "lineClassNames": { "0": [ "theme-code-block-highlighted-line", "theme-code-block-highlighted-line", ], "1": [ "theme-code-block-highlighted-line", ], "2": [ "theme-code-block-highlighted-line", ], }, } `); }); it('respects language', () => { expect( parseLines( `# highlight-next-line aaaaa bbbbb`, {metastring: '', language: 'js', magicComments: defaultMagicComments}, ), ).toMatchInlineSnapshot(` { "code": "# highlight-next-line aaaaa bbbbb", "lineClassNames": {}, } `); expect( parseLines( `/* highlight-next-line */ aaaaa bbbbb`, {metastring: '', language: 'py', magicComments: defaultMagicComments}, ), ).toMatchInlineSnapshot(` { "code": "/* highlight-next-line */ aaaaa bbbbb", "lineClassNames": {}, } `); expect( parseLines( `// highlight-next-line aaaa /* highlight-next-line */ bbbbb # highlight-next-line ccccc dddd`, {metastring: '', language: 'py', magicComments: defaultMagicComments}, ), ).toMatchInlineSnapshot(` { "code": "// highlight-next-line aaaa /* highlight-next-line */ bbbbb ccccc dddd", "lineClassNames": { "4": [ "theme-code-block-highlighted-line", ], }, } `); expect( parseLines( `// highlight-next-line aaaa /* highlight-next-line */ bbbbb # highlight-next-line ccccc dddd`, {metastring: '', language: '', magicComments: defaultMagicComments}, ), ).toMatchInlineSnapshot(` { "code": "aaaa bbbbb ccccc dddd", "lineClassNames": { "0": [ "theme-code-block-highlighted-line", ], "1": [ "theme-code-block-highlighted-line", ], "2": [ "theme-code-block-highlighted-line", ], "3": [ "theme-code-block-highlighted-line", ], }, } `); expect( parseLines( `// highlight-next-line aaaa {/* highlight-next-line */} bbbbb dddd`, {metastring: '', language: 'jsx', magicComments: defaultMagicComments}, ), ).toMatchInlineSnapshot(` { "code": "aaaa bbbbb dddd", "lineClassNames": { "0": [ "theme-code-block-highlighted-line", ], "1": [ "theme-code-block-highlighted-line", ], }, } `); expect( parseLines( `// highlight-next-line aaaa {/* highlight-next-line */} bbbbb dddd`, {metastring: '', language: 'html', magicComments: defaultMagicComments}, ), ).toMatchInlineSnapshot(` { "code": "aaaa {/* highlight-next-line */} bbbbb dddd", "lineClassNames": { "0": [ "theme-code-block-highlighted-line", ], "3": [ "theme-code-block-highlighted-line", ], }, } `); expect( parseLines( `--- # highlight-next-line aaa: boo --- aaaa
{/* highlight-next-line */} foo
bbbbb dddd \`\`\`js // highlight-next-line console.log("preserved"); \`\`\` `, {metastring: '', language: 'md', magicComments: defaultMagicComments}, ), ).toMatchInlineSnapshot(` { "code": "--- aaa: boo --- aaaa
foo
bbbbb dddd \`\`\`js // highlight-next-line console.log("preserved"); \`\`\`", "lineClassNames": { "1": [ "theme-code-block-highlighted-line", ], "11": [ "theme-code-block-highlighted-line", ], "7": [ "theme-code-block-highlighted-line", ], }, } `); }); it('parses multiple types of magic comments', () => { expect( parseLines( ` // highlight-next-line highlighted // collapse-next-line collapsed /* collapse-start */ collapsed collapsed /* collapse-end */ `, { language: 'js', metastring: '', magicComments: [ { className: 'highlight', line: 'highlight-next-line', block: {start: 'highlight-start', end: 'highlight-end'}, }, { className: 'collapse', line: 'collapse-next-line', block: {start: 'collapse-start', end: 'collapse-end'}, }, ], }, ), ).toMatchInlineSnapshot(` { "code": " highlighted collapsed collapsed collapsed", "lineClassNames": { "1": [ "highlight", ], "2": [ "collapse", ], "3": [ "collapse", ], "4": [ "collapse", ], }, } `); }); it('handles one line with multiple class names', () => { expect( parseLines( ` // highlight-next-line // collapse-next-line highlighted and collapsed /* collapse-start */ /* highlight-start */ highlighted and collapsed highlighted and collapsed /* collapse-end */ Only highlighted /* highlight-end */ /* collapse-start */ Only collapsed /* highlight-start */ highlighted and collapsed highlighted and collapsed /* highlight-end */ Only collapsed // highlight-next-line highlighted and collapsed /* collapse-end */ `, { language: 'js', metastring: '', magicComments: [ { className: 'highlight', line: 'highlight-next-line', block: {start: 'highlight-start', end: 'highlight-end'}, }, { className: 'collapse', line: 'collapse-next-line', block: {start: 'collapse-start', end: 'collapse-end'}, }, ], }, ), ).toMatchInlineSnapshot(` { "code": " highlighted and collapsed highlighted and collapsed highlighted and collapsed Only highlighted Only collapsed highlighted and collapsed highlighted and collapsed Only collapsed highlighted and collapsed", "lineClassNames": { "1": [ "highlight", "collapse", ], "2": [ "highlight", "collapse", ], "3": [ "highlight", "collapse", ], "4": [ "highlight", ], "5": [ "collapse", ], "6": [ "highlight", "collapse", ], "7": [ "highlight", "collapse", ], "8": [ "collapse", ], "9": [ "highlight", "collapse", ], }, } `); expect( parseLines( `// a // b // c // d line // b // d line `, { language: 'js', metastring: '', magicComments: [ {className: 'a', line: 'a'}, {className: 'b', line: 'b'}, {className: 'c', line: 'c'}, {className: 'd', line: 'd'}, ], }, ), ).toMatchInlineSnapshot(` { "code": "line line", "lineClassNames": { "0": [ "a", "b", "c", "d", ], "1": [ "b", "d", ], }, } `); }); it('handles CRLF line breaks with highlight comments correctly', () => { expect( parseLines( `aaaaa\r\n// highlight-start\r\nbbbbb\r\n// highlight-end\r\n`, { metastring: '', language: 'js', magicComments: defaultMagicComments, }, ), ).toMatchInlineSnapshot(` { "code": "aaaaa bbbbb", "lineClassNames": { "1": [ "theme-code-block-highlighted-line", ], }, } `); }); it('handles CRLF line breaks with highlight metastring', () => { expect( parseLines(`aaaaa\r\nbbbbb\r\n`, { metastring: '{2}', language: 'js', magicComments: defaultMagicComments, }), ).toMatchInlineSnapshot(` { "code": "aaaaa bbbbb", "lineClassNames": { "1": [ "theme-code-block-highlighted-line", ], }, } `); }); }); describe('getLineNumbersStart', () => { it('with nothing set', () => { expect( getLineNumbersStart({ showLineNumbers: undefined, metastring: undefined, }), ).toMatchInlineSnapshot(`undefined`); expect( getLineNumbersStart({ showLineNumbers: undefined, metastring: '', }), ).toMatchInlineSnapshot(`undefined`); }); describe('handles prop', () => { describe('combined with metastring', () => { it('set to true', () => { expect( getLineNumbersStart({ showLineNumbers: true, metastring: 'showLineNumbers=2', }), ).toMatchInlineSnapshot(`1`); }); it('set to false', () => { expect( getLineNumbersStart({ showLineNumbers: false, metastring: 'showLineNumbers=2', }), ).toMatchInlineSnapshot(`undefined`); }); it('set to number', () => { expect( getLineNumbersStart({ showLineNumbers: 10, metastring: 'showLineNumbers=2', }), ).toMatchInlineSnapshot(`10`); }); }); describe('standalone', () => { it('set to true', () => { expect( getLineNumbersStart({ showLineNumbers: true, metastring: undefined, }), ).toMatchInlineSnapshot(`1`); }); it('set to false', () => { expect( getLineNumbersStart({ showLineNumbers: false, metastring: undefined, }), ).toMatchInlineSnapshot(`undefined`); }); it('set to number', () => { expect( getLineNumbersStart({ showLineNumbers: 10, metastring: undefined, }), ).toMatchInlineSnapshot(`10`); }); }); }); describe('handles metadata', () => { describe('standalone', () => { it('set as flag', () => { expect( getLineNumbersStart({ showLineNumbers: undefined, metastring: 'showLineNumbers', }), ).toMatchInlineSnapshot(`1`); }); it('set with number', () => { expect( getLineNumbersStart({ showLineNumbers: undefined, metastring: 'showLineNumbers=10', }), ).toMatchInlineSnapshot(`10`); }); }); describe('combined with other options', () => { it('set as flag', () => { expect( getLineNumbersStart({ showLineNumbers: undefined, metastring: '{1,2-3} title="file.txt" showLineNumbers noInline', }), ).toMatchInlineSnapshot(`1`); }); it('set with number', () => { expect( getLineNumbersStart({ showLineNumbers: undefined, metastring: '{1,2-3} title="file.txt" showLineNumbers=10 noInline', }), ).toMatchInlineSnapshot(`10`); }); }); }); }); describe('createCodeBlockMetadata', () => { type Params = Parameters[0]; const defaultParams: Params = { code: '', className: undefined, metastring: '', language: undefined, defaultLanguage: undefined, magicComments: defaultMagicComments, title: undefined, showLineNumbers: undefined, }; function create(params?: Partial) { return createCodeBlockMetadata({...defaultParams, ...params}); } it('creates basic metadata', () => { const meta = create(); expect(meta).toMatchInlineSnapshot(` { "className": "language-text", "code": "", "codeInput": "", "language": "text", "lineClassNames": {}, "lineNumbersStart": undefined, "title": undefined, } `); }); describe('language', () => { it('returns input language', () => { const meta = create({language: 'js'}); expect(meta.language).toBe('js'); }); it('returns className language', () => { const meta = create({className: 'x language-ts y z'}); expect(meta.language).toBe('ts'); }); it('returns default language', () => { const meta = create({defaultLanguage: 'jsx'}); expect(meta.language).toBe('jsx'); }); it('returns fallback language', () => { const meta = create(); expect(meta.language).toBe('text'); }); it('returns language with expected precedence', () => { expect( create({ language: 'js', className: 'x language-ts y z', defaultLanguage: 'jsx', }).language, ).toBe('js'); expect( create({ language: undefined, className: 'x language-ts y z', defaultLanguage: 'jsx', }).language, ).toBe('ts'); expect( create({ language: undefined, className: 'x y z', defaultLanguage: 'jsx', }).language, ).toBe('jsx'); expect( create({ language: undefined, className: 'x y z', defaultLanguage: undefined, }).language, ).toBe('text'); }); }); describe('code highlighting', () => { it('returns code with no highlighting', () => { const code = 'const x = 42;'; const meta = create({code}); expect(meta.codeInput).toBe(code); expect(meta.code).toBe(code); expect(meta.lineClassNames).toMatchInlineSnapshot(`{}`); }); it('returns code with metastring highlighting', () => { const code = 'const x = 42;'; const meta = create({code, metastring: '{1}'}); expect(meta.codeInput).toBe(code); expect(meta.code).toBe(code); expect(meta.lineClassNames).toMatchInlineSnapshot( ` { "0": [ "theme-code-block-highlighted-line", ], } `, ); }); it('returns code with magic comment highlighting', () => { const code = 'const x = 42;'; const inputCode = `// highlight-next-line\n${code}`; const meta = create({code: inputCode}); expect(meta.codeInput).toBe(inputCode); expect(meta.code).toBe(code); expect(meta.lineClassNames).toMatchInlineSnapshot( ` { "0": [ "theme-code-block-highlighted-line", ], } `, ); }); }); describe('className', () => { it('returns provided className with current language', () => { const meta = create({language: 'js', className: 'some-class'}); expect(meta.className).toBe('some-class language-js'); }); it('returns provided className with fallback language', () => { const meta = create({className: 'some-class'}); expect(meta.className).toBe('some-class language-text'); }); it('returns provided className without duplicating className language', () => { const meta = create({ language: 'js', className: 'some-class language-js', }); expect(meta.className).toBe('some-class language-js'); }); }); describe('title', () => { it('returns no title', () => { const meta = create(); expect(meta.title).toBeUndefined(); }); it('returns title from metastring', () => { const meta = create({metastring: "title='my title meta'"}); expect(meta.title).toBe('my title meta'); }); it('returns title from param', () => { const meta = create({title: 'my title param'}); expect(meta.title).toBe('my title param'); }); it('returns title from meta over params', () => { const meta = create({ metastring: "title='my title meta'", title: 'my title param', }); expect(meta.title).toBe('my title meta'); }); }); describe('showLineNumbers', () => { it('returns no lineNumbersStart', () => { const meta = create(); expect(meta.lineNumbersStart).toBeUndefined(); }); it('returns lineNumbersStart - params.showLineNumbers=true', () => { const meta = create({showLineNumbers: true}); expect(meta.lineNumbersStart).toBe(1); }); it('returns lineNumbersStart - params.showLineNumbers=3', () => { const meta = create({showLineNumbers: 3}); expect(meta.lineNumbersStart).toBe(3); }); it('returns lineNumbersStart - meta showLineNumbers', () => { const meta = create({metastring: 'showLineNumbers'}); expect(meta.lineNumbersStart).toBe(1); }); it('returns lineNumbersStart - meta showLineNumbers=2', () => { const meta = create({metastring: 'showLineNumbers=2'}); expect(meta.lineNumbersStart).toBe(2); }); it('returns lineNumbersStart - params.showLineNumbers=3 + meta showLineNumbers=2', () => { const meta = create({ showLineNumbers: 3, metastring: 'showLineNumbers=2', }); expect(meta.lineNumbersStart).toBe(3); }); }); });