• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

asjqkkkk / markdown_widget / 20895381109

11 Jan 2026 12:26PM UTC coverage: 98.355%. First build
20895381109

push

github

web-flow
fix: add wrapCode for code block node (#253)

* feat: add wrapCode for code block

* chore: update example version

* fix: add unit testing by AI

* chore: add flutter testing in action file

15 of 16 new or added lines in 1 file covered. (93.75%)

837 of 851 relevant lines covered (98.35%)

8.43 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

97.37
/lib/widget/blocks/leaf/code_block.dart
1
import 'package:flutter/foundation.dart';
2
import 'package:flutter/material.dart';
3
import 'package:flutter_highlight/themes/a11y-dark.dart';
4
import 'package:flutter_highlight/themes/a11y-light.dart';
5
import 'package:highlight/highlight.dart' as hi;
6
import 'package:markdown_widget/markdown_widget.dart';
7
import 'package:markdown/markdown.dart' as m;
8

9
///Tag: [MarkdownTag.pre]
10
///
11
///An indented code block is composed of one or more indented chunks separated by blank lines
12
///A code fence is a sequence of at least three consecutive backtick characters (`) or tildes (~)
13
class CodeBlockNode extends ElementNode {
14
  CodeBlockNode(this.element, this.preConfig, this.visitor);
5✔
15

16
  String get content => element.textContent;
15✔
17
  final PreConfig preConfig;
18
  final m.Element element;
19
  final WidgetVisitor visitor;
20

21
  @override
5✔
22
  InlineSpan build() {
23
    String? language = preConfig.language;
10✔
24
    try {
25
      final languageValue =
26
          (element.children?.first as m.Element).attributes['class'];
25✔
27
      if (languageValue != null) {
28
        language = languageValue.split('-').last;
8✔
29
      }
30
    } catch (e) {
31
      language = null;
32
      debugPrint('get language error:$e');
×
33
    }
34
    final splitContents = content
5✔
35
        .trim()
5✔
36
        .split(visitor.splitRegExp ?? WidgetVisitor.defaultSplitRegExp);
16✔
37
    if (splitContents.last.isEmpty) splitContents.removeLast();
12✔
38
    final codeBuilder = preConfig.builder;
10✔
39
    if (codeBuilder != null) {
40
      return WidgetSpan(child: codeBuilder.call(content, language ?? ''));
6✔
41
    }
42

43
    final codeContent = Column(
5✔
44
      crossAxisAlignment: CrossAxisAlignment.start,
45
      children: List.generate(splitContents.length, (index) {
15✔
46
        final currentContent = splitContents[index];
5✔
47
        return ProxyRichText(
5✔
48
          TextSpan(
5✔
49
            children: highLightSpans(
5✔
50
              currentContent,
NEW
51
              language: language ?? preConfig.language,
×
52
              theme: preConfig.theme,
10✔
53
              textStyle: style,
5✔
54
              styleNotMatched: preConfig.styleNotMatched,
10✔
55
            ),
56
          ),
57
          richTextBuilder:
58
              preConfig.richTextBuilder ?? visitor.richTextBuilder,
20✔
59
        );
60
      }),
61
    );
62

63
    final widget = Container(
5✔
64
      decoration: preConfig.decoration,
10✔
65
      margin: preConfig.margin,
10✔
66
      padding: preConfig.padding,
10✔
67
      width: double.infinity,
68
      child: preConfig.wrapCode
10✔
69
          ? codeContent
70
          : SingleChildScrollView(
5✔
71
              scrollDirection: Axis.horizontal,
72
              child: codeContent,
73
            ),
74
    );
75
    return WidgetSpan(
5✔
76
        child:
77
            preConfig.wrapper?.call(widget, content, language ?? '') ?? widget);
12✔
78
  }
79

80
  @override
5✔
81
  TextStyle get style => preConfig.textStyle.merge(parentStyle);
20✔
82
}
83

84
///transform code to highlight code
85
List<InlineSpan> highLightSpans(
5✔
86
  String input, {
87
  String? language,
88
  bool autoDetectionLanguage = false,
89
  Map<String, TextStyle> theme = const {},
90
  TextStyle? textStyle,
91
  TextStyle? styleNotMatched,
92
  int tabSize = 8,
93
}) {
94
  return convertHiNodes(
5✔
95
      hi.highlight
5✔
96
          .parse(input.trimRight(),
10✔
97
              language: autoDetectionLanguage ? null : language,
98
              autoDetection: autoDetectionLanguage)
99
          .nodes!,
5✔
100
      theme,
101
      textStyle,
102
      styleNotMatched);
103
}
104

105
List<TextSpan> convertHiNodes(
6✔
106
  List<hi.Node> nodes,
107
  Map<String, TextStyle> theme,
108
  TextStyle? style,
109
  TextStyle? styleNotMatched,
110
) {
111
  List<TextSpan> spans = [];
6✔
112
  var currentSpans = spans;
113
  List<List<TextSpan>> stack = [];
6✔
114

115
  void traverse(hi.Node node, TextStyle? parentStyle) {
6✔
116
    final nodeStyle = parentStyle ?? theme[node.className ?? ''];
12✔
117
    final finallyStyle = (nodeStyle ?? styleNotMatched)?.merge(style);
4✔
118
    if (node.value != null) {
6✔
119
      currentSpans.add(node.className == null
12✔
120
          ? TextSpan(text: node.value, style: finallyStyle)
10✔
121
          : TextSpan(text: node.value, style: finallyStyle));
2✔
122
    } else if (node.children != null) {
4✔
123
      List<TextSpan> tmp = [];
4✔
124
      currentSpans.add(TextSpan(children: tmp, style: finallyStyle));
8✔
125
      stack.add(currentSpans);
4✔
126
      currentSpans = tmp;
127

128
      for (var n in node.children!) {
12✔
129
        traverse(n, nodeStyle);
4✔
130
        if (n == node.children!.last) {
12✔
131
          currentSpans = stack.isEmpty ? spans : stack.removeLast();
8✔
132
        }
133
      }
134
    }
135
  }
136

137
  for (var node in nodes) {
12✔
138
    traverse(node, null);
6✔
139
  }
140
  return spans;
141
}
142

143
///config class for pre
144
class PreConfig implements LeafConfig {
145
  final EdgeInsetsGeometry padding;
146
  final Decoration decoration;
147
  final EdgeInsetsGeometry margin;
148
  final TextStyle textStyle;
149

150
  /// the [styleNotMatched] is used to set a default TextStyle for code that does not match any theme.
151
  final TextStyle? styleNotMatched;
152
  final CodeWrapper? wrapper;
153
  final CodeBuilder? builder;
154
  final RichTextBuilder? richTextBuilder;
155

156
  ///see package:flutter_highlight/themes/
157
  final Map<String, TextStyle> theme;
158
  final String language;
159

160
  ///Whether to wrap the code when it exceeds the width of the code block.
161
  ///If false (default), the code will be horizontally scrollable.
162
  ///If true, the code will wrap to fit the width.
163
  final bool wrapCode;
164

165
  const PreConfig({
42✔
166
    this.padding = const EdgeInsets.all(16.0),
167
    this.decoration = const BoxDecoration(
168
      color: Color(0xffeff1f3),
169
      borderRadius: BorderRadius.all(Radius.circular(8.0)),
170
    ),
171
    this.margin = const EdgeInsets.symmetric(vertical: 8.0),
172
    this.textStyle = const TextStyle(fontSize: 16),
173
    this.styleNotMatched,
174
    this.theme = a11yLightTheme,
175
    this.language = 'dart',
176
    this.wrapper,
177
    this.builder,
178
    this.richTextBuilder,
179
    this.wrapCode = false,
180
  }) : assert(builder == null || wrapper == null);
4✔
181

182
  static PreConfig get darkConfig => const PreConfig(
5✔
183
        decoration: BoxDecoration(
184
          color: Color(0xff555555),
185
          borderRadius: BorderRadius.all(Radius.circular(8)),
186
        ),
187
        theme: a11yDarkTheme,
188
      );
189

190
  ///copy by other params
191
  PreConfig copy({
3✔
192
    EdgeInsetsGeometry? padding,
193
    Decoration? decoration,
194
    EdgeInsetsGeometry? margin,
195
    TextStyle? textStyle,
196
    TextStyle? styleNotMatched,
197
    CodeWrapper? wrapper,
198
    Map<String, TextStyle>? theme,
199
    String? language,
200
    RichTextBuilder? richTextBuilder,
201
    bool? wrapCode,
202
  }) {
203
    return PreConfig(
3✔
204
      padding: padding ?? this.padding,
3✔
205
      decoration: decoration ?? this.decoration,
3✔
206
      margin: margin ?? this.margin,
3✔
207
      textStyle: textStyle ?? this.textStyle,
3✔
208
      styleNotMatched: styleNotMatched ?? this.styleNotMatched,
3✔
209
      wrapper: wrapper ?? this.wrapper,
3✔
210
      theme: theme ?? this.theme,
3✔
211
      language: language ?? this.language,
1✔
212
      richTextBuilder: richTextBuilder ?? this.richTextBuilder,
3✔
213
      wrapCode: wrapCode ?? this.wrapCode,
1✔
214
    );
215
  }
216

217
  @nonVirtual
6✔
218
  @override
219
  String get tag => MarkdownTag.pre.name;
6✔
220
}
221

222
typedef CodeWrapper = Widget Function(
223
  Widget child,
224
  String code,
225
  String language,
226
);
227

228
typedef CodeBuilder = Widget Function(String code, String language);
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc