import 'package:flutter/material.dart'; import 'package:highlighter/highlighter.dart' show highlight, Node; import 'package:apidash/consts.dart'; import 'error_message.dart'; (String, bool) sanitize(String input) { bool limitedLines = false; int tabSize = 4; var lines = kSplitter.convert(input); if (lines.length > kCodePreviewLinesLimit) { lines = lines.sublist(0, kCodePreviewLinesLimit); limitedLines = true; } var replaced = lines.map((e) { if (e.startsWith("\t")) { return e.replaceAll('\t', ' ' * tabSize); } else { return e; } }); return (replaced.join('\n'), limitedLines); } class CodePreviewer extends StatefulWidget { const CodePreviewer({ super.key, required this.code, required this.theme, this.language, this.textStyle, this.padding = EdgeInsets.zero, }); final String code; final String? language; final TextStyle? textStyle; final EdgeInsetsGeometry padding; final Map theme; @override State createState() => _CodePreviewerState(); } class _CodePreviewerState extends State { late Future> spans; static const _rootKey = 'root'; static const _defaultFontColor = Color(0xff000000); late final (String, bool) processed; late TextStyle textStyle; final ScrollController controllerH = ScrollController(); final ScrollController controllerV = ScrollController(); @override void dispose() { controllerH.dispose(); controllerV.dispose(); super.dispose(); } @override void initState() { super.initState(); textStyle = TextStyle( color: widget.theme[_rootKey]?.color ?? _defaultFontColor, ); if (widget.textStyle != null) { textStyle = textStyle.merge(widget.textStyle); } processed = sanitize(widget.code); spans = asyncGenerateSpans( processed.$1, widget.language, widget.theme, processed.$2, ); } @override Widget build(BuildContext context) { return Padding( padding: widget.padding, child: FutureBuilder( future: spans, builder: (BuildContext context, AsyncSnapshot> snapshot) { if (snapshot.hasData && snapshot.connectionState == ConnectionState.done) { var finalSpans = snapshot.data!; return Scrollbar( thickness: 10, thumbVisibility: true, controller: controllerV, child: Scrollbar( notificationPredicate: (notification) => notification.depth == 1, thickness: 10, thumbVisibility: true, controller: controllerH, child: SingleChildScrollView( controller: controllerV, child: SingleChildScrollView( scrollDirection: Axis.horizontal, controller: controllerH, child: Column( children: [ Row( children: [ SelectionArea( child: Text.rich( TextSpan( children: finalSpans, style: textStyle, ), softWrap: false, //selectionRegistrar: // SelectionContainer.maybeOf(context), //selectionColor: const Color(0xAF6694e8), ), ), ], ), ], ), ), ), ), ); } if (snapshot.hasError) { return ErrorMessage(message: snapshot.error.toString()); } return const CircularProgressIndicator(); }, ), ); } } Future> asyncGenerateSpans(String code, String? language, Map theme, bool limitedLines) async { var parsed = highlight.parse(code, language: language); var spans = convert(parsed.nodes!, theme); if (limitedLines) { spans.add(const TextSpan( text: "\n... more.\nPreview ends here ($kCodePreviewLinesLimit lines).\nYou can check Raw for full result.")); } return spans; } List convert(List nodes, Map theme) { final List spans = []; var currentSpans = spans; final List> stack = []; void traverse(Node node) { var val = node.value; final nodeChildren = node.children; final nodeStyle = theme[node.className]; if (val != null) { currentSpans.add(TextSpan(text: val, style: nodeStyle)); } else if (nodeChildren != null) { List tmp = []; currentSpans.add(TextSpan(children: tmp, style: nodeStyle)); stack.add(currentSpans); currentSpans = tmp; for (final n in nodeChildren) { traverse(n); if (n == nodeChildren.last) { currentSpans = stack.isEmpty ? spans : stack.removeLast(); } } } } for (var node in nodes) { traverse(node); } return spans; }