Skip to content

Commit adf3fe7

Browse files
authored
[markdown] Preserve metadata passed to fenced code blocks (#2186)
1 parent b435a46 commit adf3fe7

File tree

5 files changed

+79
-20
lines changed

5 files changed

+79
-20
lines changed

pkgs/markdown/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
## 7.3.1-wip
22

3+
* Preserve metadata passed to fenced code blocks as
4+
`data-metadata` on the created `pre` element.
35
* Update the README link to the markdown playground
46
(https://dart-lang.github.io/tools).
57
* Update `package:web` API references in the example.

pkgs/markdown/lib/src/block_syntaxes/fenced_code_block_syntax.dart

Lines changed: 49 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -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
}

pkgs/markdown/test/common_mark/fenced_code_blocks.unit

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -214,7 +214,7 @@ def foo(x)
214214
end
215215
~~~~~~~
216216
<<<
217-
<pre><code class="language-ruby">def foo(x)
217+
<pre data-metadata="startline=3 $%@#$"><code class="language-ruby">def foo(x)
218218
return 3
219219
end
220220
</code></pre>
@@ -234,7 +234,7 @@ foo</p>
234234
foo
235235
~~~
236236
<<<
237-
<pre><code class="language-aa">foo
237+
<pre data-metadata="``` ~~~"><code class="language-aa">foo
238238
</code></pre>
239239
>>> Fenced code blocks - 147
240240
```

pkgs/markdown/test/gfm/fenced_code_blocks.unit

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -214,7 +214,7 @@ def foo(x)
214214
end
215215
~~~~~~~
216216
<<<
217-
<pre><code class="language-ruby">def foo(x)
217+
<pre data-metadata="startline=3 $%@#$"><code class="language-ruby">def foo(x)
218218
return 3
219219
end
220220
</code></pre>
@@ -234,7 +234,7 @@ foo</p>
234234
foo
235235
~~~
236236
<<<
237-
<pre><code class="language-aa">foo
237+
<pre data-metadata="``` ~~~"><code class="language-aa">foo
238238
</code></pre>
239239
>>> Fenced code blocks - 117
240240
```

pkgs/markdown/test/original/fenced_code_block.unit

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,27 @@
55
<<<
66
<pre><code>'foo'
77
</code></pre>
8+
>>> with basic metadata string
9+
```dart meta
10+
code
11+
```
12+
13+
<<<
14+
<pre data-metadata="meta"><code class="language-dart">code
15+
</code></pre>
16+
>>> with characters to escape
17+
```dart title="main.dart"
18+
code
19+
```
20+
21+
<<<
22+
<pre data-metadata="title=&quot;main.dart&quot;"><code class="language-dart">code
23+
</code></pre>
24+
>>> with HTML character reference
25+
```dart &#124;
26+
code
27+
```
28+
29+
<<<
30+
<pre data-metadata="|"><code class="language-dart">code
31+
</code></pre>

0 commit comments

Comments
 (0)