Skip to content

Commit 9801c5b

Browse files
add codeblock
1 parent 6726c4c commit 9801c5b

6 files changed

Lines changed: 402 additions & 0 deletions

File tree

cms/scripts/migrate-blog-to-components.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import './sync-mdx/videoEmbedHandler'
2424
import './sync-mdx/blockquoteHandler'
2525
import './sync-mdx/calloutTextHandler'
2626
import './sync-mdx/pdfEmbedHandler'
27+
import './sync-mdx/codeBlockHandler'
2728
import './sync-mdx/ambassadorHandler'
2829

2930
import { parseMdxToBlocks, type ParserContext } from './sync-mdx/mdxBlockParser'
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
import { describe, it, expect } from 'vitest'
2+
import { parseMdxToBlocks } from './mdxBlockParser'
3+
import { MdxParserError, ParserErrorCode } from './parserErrors'
4+
5+
// Side-effect import: registers CodeBlock handler
6+
import './codeBlockHandler'
7+
8+
const ctx = { locale: 'en' }
9+
10+
describe('CodeBlock handler', () => {
11+
it('parses language, title, and template literal code', async () => {
12+
const blocks = await parseMdxToBlocks(
13+
'<CodeBlock language="html" title="test" code={`qwe`} />',
14+
ctx
15+
)
16+
17+
expect(blocks).toEqual([
18+
{
19+
__component: 'blocks.code-block',
20+
code: 'qwe',
21+
language: 'html',
22+
title: 'test'
23+
}
24+
])
25+
})
26+
27+
it('parses multiline code from a template literal', async () => {
28+
const blocks = await parseMdxToBlocks(
29+
`<CodeBlock language="python" title="hello.py" code={\`def hello():
30+
print("world")\`} />`,
31+
ctx
32+
)
33+
34+
expect(blocks[0]).toMatchObject({
35+
__component: 'blocks.code-block',
36+
language: 'python',
37+
title: 'hello.py',
38+
code: 'def hello():\n print("world")'
39+
})
40+
})
41+
42+
it('normalizes 4-space indentation to 2 spaces', async () => {
43+
const blocks = await parseMdxToBlocks(
44+
`<CodeBlock language="javascript" code={\`function run() {
45+
if (true) {
46+
return 1
47+
}
48+
}\`} />`,
49+
ctx
50+
)
51+
52+
expect((blocks[0] as { code: string }).code).toBe(
53+
'function run() {\n if (true) {\n return 1\n }\n}'
54+
)
55+
})
56+
57+
it('omits title when not provided', async () => {
58+
const blocks = await parseMdxToBlocks(
59+
'<CodeBlock language="javascript" code={`const x = 1`} />',
60+
ctx
61+
)
62+
63+
expect(blocks[0]).toEqual({
64+
__component: 'blocks.code-block',
65+
code: 'const x = 1',
66+
language: 'javascript'
67+
})
68+
})
69+
70+
it('preserves markdown order around CodeBlock', async () => {
71+
const blocks = await parseMdxToBlocks(
72+
`Intro paragraph.
73+
74+
<CodeBlock language="bash" code={\`echo hi\`} />
75+
76+
Outro paragraph.`,
77+
ctx
78+
)
79+
80+
expect(blocks).toHaveLength(3)
81+
expect(blocks[0]).toMatchObject({ __component: 'blocks.paragraph' })
82+
expect(blocks[1]).toMatchObject({
83+
__component: 'blocks.code-block',
84+
code: 'echo hi',
85+
language: 'bash'
86+
})
87+
expect(blocks[2]).toMatchObject({ __component: 'blocks.paragraph' })
88+
})
89+
})
90+
91+
describe('CodeBlock handler — errors', () => {
92+
it('returns MISSING_REQUIRED_PROP when language is missing', async () => {
93+
const result = await parseMdxToBlocks(
94+
'<CodeBlock code={`const x = 1`} />',
95+
ctx
96+
)
97+
expect(result).toBeInstanceOf(MdxParserError)
98+
expect(result).toMatchObject({
99+
code: ParserErrorCode.MISSING_REQUIRED_PROP
100+
})
101+
})
102+
103+
it('returns MISSING_REQUIRED_PROP when code is missing', async () => {
104+
const result = await parseMdxToBlocks(
105+
'<CodeBlock language="javascript" />',
106+
ctx
107+
)
108+
expect(result).toBeInstanceOf(MdxParserError)
109+
expect(result).toMatchObject({
110+
code: ParserErrorCode.MISSING_REQUIRED_PROP
111+
})
112+
})
113+
114+
it('returns INVALID_PROP_VALUE for unsupported language', async () => {
115+
const result = await parseMdxToBlocks(
116+
'<CodeBlock language="cobol" code={`x`} />',
117+
ctx
118+
)
119+
expect(result).toBeInstanceOf(MdxParserError)
120+
expect(result).toMatchObject({
121+
code: ParserErrorCode.INVALID_PROP_VALUE
122+
})
123+
})
124+
125+
it('returns DYNAMIC_EXPRESSION for template interpolation', async () => {
126+
const result = await parseMdxToBlocks(
127+
'<CodeBlock language="javascript" code={`hello ${name}`} />',
128+
ctx
129+
)
130+
expect(result).toBeInstanceOf(MdxParserError)
131+
expect(result).toMatchObject({
132+
code: ParserErrorCode.DYNAMIC_EXPRESSION
133+
})
134+
})
135+
})
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import type { CodeBlockBlock, ParsedBlock } from './types.blocks'
2+
import { getStaticExpressionAttr, getStringAttr } from './jsxExtract'
3+
import {
4+
registerComponentHandler,
5+
type JsxBlockNode,
6+
type ParserContext
7+
} from './mdxBlockParser'
8+
import { MdxParserError, ParserErrorCode, tryCatchParserError } from './parserErrors'
9+
10+
const CODE_BLOCK_LANGUAGES = [
11+
'javascript',
12+
'typescript',
13+
'jsx',
14+
'tsx',
15+
'html',
16+
'css',
17+
'bash',
18+
'json',
19+
'yaml',
20+
'python',
21+
'rust',
22+
'go',
23+
'sql',
24+
'markdown'
25+
] as const
26+
27+
type CodeBlockLanguage = (typeof CODE_BLOCK_LANGUAGES)[number]
28+
29+
function normalizeCodeIndent(code: string): string {
30+
return code.replace(/^((?: {4})+)/gm, (indent) =>
31+
' '.repeat(indent.length / 4)
32+
)
33+
}
34+
35+
async function handleCodeBlock(
36+
node: JsxBlockNode,
37+
ctx: ParserContext
38+
): Promise<ParsedBlock[] | MdxParserError> {
39+
return tryCatchParserError(() => {
40+
const language = getStringAttr(node, 'language', { required: true })
41+
const title = getStringAttr(node, 'title')
42+
const code = getStaticExpressionAttr(node, 'code', {
43+
required: true,
44+
sourceText: ctx.sourceText
45+
})
46+
47+
if (!CODE_BLOCK_LANGUAGES.includes(language as CodeBlockLanguage)) {
48+
throw new MdxParserError({
49+
code: ParserErrorCode.INVALID_PROP_VALUE,
50+
message: `Prop "language" must be one of: ${CODE_BLOCK_LANGUAGES.join(', ')}.`,
51+
component: node.name ?? undefined,
52+
prop: 'language',
53+
line: node.position?.start.line,
54+
column: node.position?.start.column
55+
})
56+
}
57+
58+
const block: CodeBlockBlock = {
59+
__component: 'blocks.code-block',
60+
code: normalizeCodeIndent(code),
61+
language
62+
}
63+
64+
if (title !== undefined) {
65+
block.title = title
66+
}
67+
68+
return [block]
69+
})
70+
}
71+
72+
registerComponentHandler('CodeBlock', handleCodeBlock)

cms/scripts/sync-mdx/config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import './ctaStripHandler'
2121
import './paragraphHandler'
2222
import './pdfEmbedHandler'
2323
import './videoEmbedHandler'
24+
import './codeBlockHandler'
2425
import { createRelationResolver } from './ambassadorHandler'
2526
import { type ParserContext } from './mdxBlockParser'
2627
import { MdxParserError, ParserErrorCode } from './parserErrors'

0 commit comments

Comments
 (0)