@@ -22,9 +22,9 @@ class FencedCodeBlockSyntax extends BlockSyntax {
2222
2323 @override
2424 Node parse (BlockParser parser) {
25- final openingFence = _FenceMatch .fromMatch (pattern. firstMatch (
26- escapePunctuation (parser.current.content),
27- )! ) ;
25+ final openingFence = _FenceMatch .fromMatch (
26+ pattern. firstMatch ( escapePunctuation (parser.current.content)) ! ,
27+ );
2828
2929 var text = parseChildLines (
3030 parser,
@@ -39,16 +39,31 @@ class FencedCodeBlockSyntax extends BlockSyntax {
3939 text = '$text \n ' ;
4040 }
4141
42+ final (languageString, metadataString) = openingFence.languageAndMetadata;
43+
4244 final code = Element .text ('code' , text);
43- if (openingFence.hasLanguage) {
44- var language = decodeHtmlCharacters (openingFence.language);
45- if (parser.document.encodeHtml) {
46- language = escapeHtmlAttribute (language);
47- }
48- code.attributes['class' ] = 'language-$language ' ;
45+ if (languageString != null ) {
46+ final processedLanguage = _processAttribute (languageString,
47+ encodeHtml: parser.document.encodeHtml);
48+ code.attributes['class' ] = 'language-$processedLanguage ' ;
49+ }
50+
51+ final pre = Element ('pre' , [code]);
52+ if (metadataString != null ) {
53+ final processedMetadata = _processAttribute (metadataString,
54+ encodeHtml: parser.document.encodeHtml);
55+ pre.attributes['data-metadata' ] = processedMetadata;
4956 }
5057
51- return Element ('pre' , [code]);
58+ return pre;
59+ }
60+
61+ static String _processAttribute (String value, {bool encodeHtml = false }) {
62+ final decodedValue = decodeHtmlCharacters (value);
63+ if (encodeHtml) {
64+ return escapeHtmlAttribute (decodedValue);
65+ }
66+ return decodedValue;
5267 }
5368
5469 @override
@@ -144,12 +159,30 @@ class _FenceMatch {
144159 // https://spec.commonmark.org/0.30/#info-string.
145160 final String info;
146161
147- // The first word of the info string is typically used to specify the language
148- // of the code sample,
149- // https://spec.commonmark.org/0.30/#example-143.
150- String get language => info.split (' ' ).first;
162+ /// Returns the language and remaining metadata from the [info] string.
163+ ///
164+ /// The language is the first word of the info string,
165+ /// to match the (unspecified, but typical) behavior of CommonMark parsers,
166+ /// as suggested in https://spec.commonmark.org/0.30/#example-143.
167+ ///
168+ /// The metadata is any remaining part of the info string after the language.
169+ (String ? language, String ? metadata) get languageAndMetadata {
170+ if (info.isEmpty) {
171+ return (null , null );
172+ }
151173
152- bool get hasInfo => info.isNotEmpty;
174+ // We assume the info string is trimmed already.
175+ final firstSpaceIndex = info.indexOf (' ' );
176+ if (firstSpaceIndex == - 1 ) {
177+ // If there is no space, the whole string is the language.
178+ return (info, null );
179+ }
153180
154- bool get hasLanguage => language.isNotEmpty;
181+ return (
182+ info.substring (0 , firstSpaceIndex),
183+ info.substring (firstSpaceIndex + 1 ),
184+ );
185+ }
186+
187+ bool get hasInfo => info.isNotEmpty;
155188}
0 commit comments